缓存击穿(热点key): 部分key(被高并发访问且缓存重建业务复杂的)失效,无数请求会直接到数据库,造成巨大压力
1.互斥锁:可以保证强一致性
线程一:未命中之后,获取互斥锁,再查询数据库重建缓存,写入缓存,释放锁
线程二:查询未命中,未获得锁(已由线程一获得),等待一会,缓存命中
互斥锁实现方式:redis中setnx key value:改变对应key的value,仅当value不存在时执行,以此来实现互斥锁,防止出现锁得不到释放,设置有效期
public Shop queryWithMutex(Long id) throws InterruptedException {
Shop shop;
//实现互斥锁,解决缓存击穿
String key=CACHE_SHOP_KEY+id;
//1.从redis查询商铺缓存
String shopJson=stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在,isNotBlank("")也为false
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回商铺对象
return JSONUtil.toBean(shopJson,Shop.class);
}
//判断命中的是否为空值
if(shopJson != null && shopJson.isEmpty()){
return null;
}
//4.实现缓存重建
String lockKey=LOCK_SHOP_KEY+id;
//4.1.获取互斥锁
boolean isLock=tryLock("lockKey");
//4.2.判断互斥锁是否成功
if(!isLock){
//4.3.未成功,等待
Thread.sleep(50);
//递归
shop=queryWithMutex(id);
}
else{
//4.4.成功,从mysql数据库中查询
shop=getById(id);
//5.判断是否存在
if(shop==null){
//缓存空值,处理缓存穿透
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//6.存在,向redis中缓存店铺数据
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),LOGIN_USER_TTL, TimeUnit.MINUTES);
//7.释放互斥锁
unlock(lockKey);
}
//8.返回
return shop;
}
2.逻辑过期:
不存TTL,添加上逻辑过期时间,判断逻辑上有没有过期,以此来更新数据
线程一:查询缓存,逻辑已过期,获取互斥锁,开启新线程,返回过期数据
新线程:查询数据库并重建缓存,重置逻辑过期时间,释放锁
线程二:查询未命中,未获得锁(已由线程一获得),返回过期数据
private boolean tryLock(String key){
//尝试获得互斥锁
Boolean flag=stringRedisTemplate.opsForValue().setIfAbsent("key","1",LOCK_SHOP_TTL,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);//通过工具类将其转化为基本类型
}
private void unlock(String key){
//删除锁
stringRedisTemplate.delete("key");
}
实现互斥锁相关的方法
//线程池,有10个线程
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
创建线程池
public Shop queryWithLogicalExpire(Long id) {
//实现逻辑过期,解决缓存击穿(不存在缓存穿透问题)
String key=CACHE_SHOP_KEY+id;
String lockKey=LOCK_SHOP_KEY+id;
//1.从redis查询商铺缓存
String shopJson=stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isBlank(shopJson)){
//3.不存在,返回null
return null;
}
//4.存在,判断是否过期
//将Json反序列化成RedisDate对象
RedisData redisData=JSONUtil.toBean(shopJson,RedisData.class);
Shop shop=JSONUtil.toBean((JSONObject)redisData.getData(),Shop.class);
//5.过期
if(LocalDateTime.now().isAfter(redisData.getExpireTime())){
//6.缓存重建
//6.1.获取互斥锁
boolean isLock=tryLock(lockKey);
//6.2.获取成功
if(isLock){
//开启新线程
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
saveShopToRedis(id, 20L);
}catch(Exception e){
throw new RuntimeException(e);
}
finally {
//释放锁
unlock(lockKey);
}
});
}
//6.3.获取失败
return shop;
}
return shop;
}
逻辑删除相关方法