缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
本问题主要针对热点key问题。
因为是热点,所以数据库中是一定存在的
缓存和数据库都不存在,那不是缓存击穿问题,是缓存穿透问题。这点要搞清楚。
特点:一致性不高,性能高,实现复杂
我自己画的图:
我们逻辑上设置一个过期时间expireTime,来判断数据是否过期。
于是,第一步
1.封装数据
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
2.缓存预热
即提前在 真实的Redis缓存 中存入数据,这个数据在Redis内存中永不过期。
这样,热点key在缓存中存储完毕。
我们人为的 在逻辑上,用 expireTime 来判断是否过期
没过期:很简单,在逻辑上 缓存中存在,直接返回
过期:较为复杂,在逻辑上 “缓存过期”了(即我们自定义对象RedisData中的expireTime过期了,并不是真实的Redis中的数据过期了)
尝试获取锁,若获取锁失败,直接返回旧数据!!!
若成功获取锁,开启独立线程 Executors.newFixedThreadPool(10);
这个独立线程就相当于找个小弟,帮自己搞定麻烦事。
小弟做事,其他人通通闪开(其他线程返直接返回旧数据)
这个小弟(独立线程)做的事:缓存重建this.saveShop2Redis(id,20L);
小弟(独立线程)帮忙重建缓存,在小弟重建缓存完成之前,其它线程返回只能返回之前的数据。这也是 逻辑过期 出现不一致问题的关键点。
这里的缓存重建,是向我们封装的 RedisData对象 中进行操作,设置过期时间expireTime。实际上、物理上,Redis内存中数据是一直存在的!
最后在finally里面完成锁的释放
正式代码:
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
String key = CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
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){
CACHE_REBUILD_EXECUTOR.submit( ()->{
try{
//重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return shop;
}