概念
缓存击穿问题也叫热点key问题,指的是一个被高并发访问并且缓存重建业务较为复杂的key突然失效了,大量的请求会到达数据库给数据库带来巨大的冲击。
常见解决方法有两种:互斥锁,逻辑过期。
优缺点 :
基于互斥锁的业务代码改造
ublic Shop queryWithMutex(Long id) {
//先从redis中查询缓存
try {
String object = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY);
//如果有直接返回
if (StrUtil.isNotBlank(object)) {
//转成Java对象
Shop bean = JSONUtil.toBean(object, Shop.class);
return bean;
}
//判断命中的是否是空值
if (object != null) {
return null;
}
//实现缓存重建
//尝试获取锁
boolean trylock = trylock(LOCK_SHOP_KEY);
if (!trylock) {
//如果获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//如果没有,查询数据库
Shop shop = this.getById(id);
//如果数据库中没有,报错
if (shop == null) {
//return Result.fail("没有该店铺");
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//如果有,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
return shop;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//关闭互斥锁
unlock(LOCK_SHOP_KEY);
}
}
//获取锁和释放锁 (基于redis的setNX实现)
private boolean trylock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
基于逻辑过期的业务代码改造
我们需要新建一个对象来封装我们的店铺信息和逻辑过期时间,所以从redis中查询到数据后需要手动反序列化程Java对象获取我们的逻辑过期时间和店铺信息。
private static final ExecutorService CACHE_REBUILD_POOL = Executors.newFixedThreadPool(10);
public Shop queryWithLogicExpire(Long id) {
//先从redis中查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY);
//如果有直接返回
if (StrUtil.isBlank(shopJson)) {
//如果不存在,直接返回
return null;
}
//命中,需要把json反序列化转成java对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject shop = (JSONObject) redisData.getData();
Shop shopBean = JSONUtil.toBean(shopJson, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期,返回旧数据
return shopBean;
}
//过期,需要缓存重建
//获取互斥锁
boolean islock = trylock(LOCK_SHOP_KEY + id);
if (islock) {
//如果获取成功,开启线程实现缓存重建
//此处应该再次判断逻辑时间是否过期
try {
CACHE_REBUILD_POOL.submit(() -> {
this.saveShop2Redis(id,20l);
});
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(LOCK_SHOP_KEY + id);
}
}
//返回旧数据
return shopBean;
}
//缓存重建
public void saveShop2Redis(Long id, Long expierSecond) {
//查询店铺信息
Shop shopById = getById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shopById);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expierSecond));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY +id + shopById.getId(), JSONUtil.toJsonStr(redisData), 30, TimeUnit.MINUTES);
}
//获取锁
private boolean trylock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}