有2种方案实现
采用方案为:Redis SETNX + 删除锁(也整合了缓存穿透问题)
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;// 它是一个对象,应该用 键的值应该是Hash,此处只是为了演示
@Resource
private RedissonClient redissonClient;
/*模拟加锁*/
private boolean tryLock(String key){
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS);
return BooleanUtil.isTrue(b);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
@Override
public Result queryById(Long id) {
// 布隆过滤器判断ID是否可能存在,防止缓存穿透
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("shop:bloom");
if (!bloomFilter.contains(id)) {
// 如果布隆过滤器判断为不存在,直接返回,避免访问缓存和数据库
return Result.fail("店铺不存在(布隆过滤器拦截)");
}
// 1. 从 Redis 查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2. 如果缓存命中,直接返回
if(StrUtil.isNotBlank(shopJson)){
log.info("shopJson缓存中有:{}",shopJson);
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 3. 如果缓存中是空字符串(说明数据库中查过确实不存在),返回错误
if (shopJson != null) {
return Result.fail("店铺信息不存在(缓存空值)");
}
// 4. 缓存未命中,准备查询数据库前先尝试加锁,防止缓存击穿
String lockKey = "shop:lock" + id;
boolean lock = tryLock(lockKey); // 尝试加锁 true 该线程拿到了锁
Shop shop;
try {
if (lock) {
// 5. 获取锁成功,查询数据库
shop = getById(id);
// 6. 数据库中也不存在,返回错误(此处未缓存空值,依赖布隆拦截)
if (shop == null) {
// return 之前会进入finally
return Result.fail("店铺不存在");
}
// 7. 查询成功,写入缓存
String jsonStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, 3L, TimeUnit.MINUTES);
log.info("shopJson写入缓存:{}", jsonStr);
} else {
// 8. 获取锁失败,稍等后递归重试(等待其他线程完成缓存填充)
Thread.sleep(50);
// return 之前会进入finally
return queryById(id);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 9. 只有拿到锁的线程才释放锁,避免误删其他线程的锁
if (lock) {
unlock(lockKey);
}
}
// 10. 返回结果
return Result.ok(shop);
}