利用互斥锁解决缓存击穿问题
1 在ShopServiceImpl类中定义一个tryLock方法(在Redis中的setnx相当于setIfAbsent方法。setnx,是「 set if Not eXists」的缩写)
2 并释放锁
//利用互斥锁解决缓存击穿问题
private boolean tryLock(String key){//String opsForValue()
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);//直接返回会做拆箱的
}
//释放锁 删除
private void unlock(String key){
stringRedisTemplate.delete(key);
}
3 ShopServiceImpl类中定义一个queryWithPassThrough方法、
//在queryById 的基础上不是返回null就是返回本身
public Shop queryWithPassThrough(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从Redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson != null){
//返回一个错误信息
return null;
}
//redis查询不存在的字段要么返回null 如果不为null则"",存在就写入redis,
//4.不存在,根据id查询数据库
Shop shop = getById(id);
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);
return shop;
}
4 ShopServiceImpl类中定义一个queryWithMutex方法:
在queryWithPassThrough基础上看图修改
private Shop queryWithMutex(Long id) {
//提交商铺id 这个key是缓存的key
String key = CACHE_SHOP_KEY + id;
//1.从Redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断缓存是否命中
if(StrUtil.isNotBlank(shopJson)){
//3.命中,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//上面是有值的情况,下面是无值的2种情况:A:空字符串。B:null。
//判断命中的是否是空值
if (shopJson != null){
//返回一个错误信息
return null;
}
//从这开始修改
//4 实现缓存重建
//4.1 获取互斥锁
//这里是锁的key
String locKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(locKey);
//4.2 判断是否获取成功
if (!isLock){
//4.3 失败 则休眠重试
Thread.sleep(50);
return queryWithMutex(id);//递归 不断循环
}
//4.4成功 则根据id查询数据库
shop = getById(id);
//为了让他更容易的发生并发这种冲突 就让他休眠一下
//模拟重建的延时
Thread.sleep(200);
//5 不存在,根据id查询数据库 redis查询不存在的字段要么返回null 如果不为null则"",存在就写入redis,
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(locKey);
}
//8 返回
return shop;
}
5 ShopServiceImpl类中修改queryById,调用queryWithMutex:
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null){
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
利用逻辑过期解决缓存击穿问题
如何添加逻辑过期字段?
可以在utils包下定义RedisData类(可以让Shop继承RedisData类),也可以在RedisData中设置一个Shop类的data属性:
public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
Shop shop = getById(id);//查询店铺数据
//缓存重建有一定的延迟
Thread.sleep(200);
// 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
//过期时间
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
}
Java 多线程:线程池
https://blog.csdn.net/u013541140/article/details/95225769
逻辑过期解决缓存穿透实现 看图
/**
* 逻辑过期解决缓存穿透实现
*/
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从Redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//未命中 返回null
return null;
}
//判断命中的是否是空值
if (shopJson != null){
//返回一个错误信息
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();//getData() 店铺信息 原本是Object 要进行强转
Shop shop = JSONUtil.toBean(data, 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){
//6.3.成功,开启独立线程,实现缓存重建 获取成功应再检测redis缓存是否过期,做DoubleCheck
//如果存在则无需重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
});
}
//6.4.失败,返回过期的商铺信息
return shop;
}