Redis 缓存击穿问题
解决方案一:互斥锁
假设一个热门产品的缓存时间到期了,那么将会有大量的请求查询不到缓存,就只能去查询数据库然后再把数据添加到缓存中。但是如果在缓存时间到期的瞬间有很多个请求都来查询这个热门产品,因为缓存当中查询不到数据,导致他们都无法得到数据,只能够去查询数据库,这样便会造成数据库的压力过大,甚至可能导致宕机。
实现流程如下:

互斥锁实现代码如下:
//首先编写一个获得锁的方法
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);
}
// 接下来编写业务方法,查询一个热门产品
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
//返回
return Result.ok(shop);
}
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
if (shopJson != null) {
return null;
}
//4.实现缓存重建
//4.1获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//4.2判断是否获取成功
if (!isLock) {
//4.3失败,则休眠并重试
Thread.sleep(50);
//递归
return queryWithMutex(id);
}
//4.4成功,根据id查询数据库
shop = getById(id);
//5.不存在,返回错误
if (shop == null) {
//缓存击穿问题
//将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁
unlock(lockKey);
}
//8.返回
return shop;
}
解决方案二:逻辑过期
逻辑过期方案:用户查询某个热门产品信息,如果缓存未命中(即信息为空),则直接返回空,不去查询数据库。如果缓存信息命中,则判断是否逻辑过期,未过期返回缓存信息,过期则重建缓存,尝试获得互斥锁,获取失败则直接返回已过期缓存数据,获取成功则开启独立线程去重构缓存然后直接返回旧的缓存信息,重构完成之后就释放互斥锁。
这样看来,只要命中了缓存,无论是否过期,是否获得锁看,都返回同一个缓存信息
实现流程如下:

逻辑过期实现代码如下:
//查询某个热门产品信息的方法
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
//Shop shop = queryWithMutex(id);
//逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
//返回
return Result.ok(shop);
}
//这里解决 缓存击穿 问题的主要业务方法写在了 queryWithLogicalExpire(id) 中,完整代码如下
//逻辑过期
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.未命中
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = (Shop) redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1还未过期
return shop;
}
//5.2已经过期,需要缓存重建
//6.缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//6.2判断是否获取锁成功
if (isLock) {
// 6.3成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
unlock(lockKey);
}
});
}
//6.4返回过期的店铺信息
//7.返回
return shop;
}
//由于线程获得锁之后要开启独立线程去重构缓存信息,那么我们最好提前声明定义一个线程池 //线程池 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//除了线程池外,我们还需要 获得锁 和 释放锁 的方法,具体代码如下
//获得锁
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);
}
原文链接:
https://blog.csdn.net/Decade_Faiz/article/details/126790237
https://blog.csdn.net/Decade_Faiz/article/details/127056332
流程图参考:https://www.bilibili.com/video/BV1cr4y1671t?p=44&vd_source=41a91db5b456307bec38f0f9b06054e4
浙公网安备 33010602011771号