文章目录
- 一、什么是缓存击穿?
- 二、解决方案
- 1.互斥锁
- 2.编码实现
- 总结
一、什么是缓存击穿?
缓存击穿问题也叫热点key问题,一个被高并发访问并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击
二、解决方案
1.互斥锁
本文采用互斥锁的方式来解决缓存击穿问题。那么什么是互斥锁呢?
如下图:
当线程1请求过来时,先查询缓存未命中就会开启互斥锁,如果这时候线程2进来会先查询缓存,未命中也会开启互斥锁,但是线程1已经开启了会获取锁失败,那么会休眠,稍后再去获取缓存。线程1在获取锁成功之后会查询数据库重建缓存,将数据存入redis中最后再释放锁。
互斥锁与悲观锁有点类似,可以参考悲观锁来理解。
在Redis中可以使用setnx命令来实现互斥锁,当lock存在的时候,无法进行设值
2.编码实现
准备两个方法,获取锁与释放锁
获取锁
//尝试获取锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
释放锁
//释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
互斥锁解决缓存击穿
**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
Shop shop = null;
try {
//1.从redis获取缓存
String jsonStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断缓存是否命中
if (StrUtil.isNotBlank(jsonStr)){
//命中,返回
return Result.ok(JSONUtil.toBean(jsonStr, new TypeReference<Shop>() {
},true));
}
//3.判断命中的是否是空字符串
if (jsonStr != null){
return Result.fail("商铺信息不存在");
}
//未命中
//4.获取互斥锁
boolean isLock = tryLock(LOCK_SHOP_KEY + id);
//5.判断是否获取锁成功
if (!isLock){
//6.失败,休眠
Thread.sleep(300);
//7.重试,递归
return queryById(id);
}
//7.根据id查询数据库
shop = this.getById(id);
//8.判断shop是否存在
if (ObjectUtil.isNull(shop)){
//不存在,将空值存入redis,返回错误信息
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("商铺信息不存在");
}
//存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//9.释放互斥锁
unlock(LOCK_SHOP_KEY + id);
}
return Result.ok(shop);
}
总结
使用互斥锁优点是没有额外的内存消耗,保证一致性,实现简单。但是也存在性能问题,每个没有获取到锁的线程都会等待有死锁的风险。
还有一种解决办法是使用逻辑过期的方法这个就解决了线程等待问题,但是这个就不保证数据的一致性,会得到过期的数据。怎样选择还是得看具体的业务场景。