基于逻辑过期进行缓存预热,解决缓存击穿问题

大体思路

所有热点数据会提前存入redis,如果缓存未命中,说明不是热点数据,可以直接返回空

​ 缓存穿透是有大量不存在数据访问redis,redis未命中然后去访问数据库,就造成了缓存穿透。

​ 这里我们只针对的热点数据,操作的是缓存,要判断的也只是数据是否过期,过期就另开一个线程重建缓存,在这个重建缓存的时间内,其他并发线程访问缓存,返回的是过期的数据,这个树据在缓存中并没有真的过期,只是存入了一个过期时间,后面判断就是根据这个过期时间进行的,所以说是逻辑过期,保证了可用性。

image-20250307202603153

实现

1,为原来的实体类添加一个字段:expireTime,这里采用的是装饰器模式,另建了一个类,当然也可以继承

1
2
3
4
5
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}

data中可以存原来的实体类

2,可以在测试类中进行缓存预热

1
2
3
4
5
6
7
8
9
10
11
12
13
//缓存预热
public void saveShopToRedis(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));

stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}

3,核心代码,开线程池加具体逻辑,这里不需要在避免缓存穿透,因为预想情况下热点数据都以进行预热,都存在缓存中,这时缓存未命中,就是不存在,可以直接返回空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//建立线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

//利用逻辑过期解决缓存击穿问题
public Shop queryWithLogicExpire(Long id){

//1,从redis中查询商铺缓存

String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);

//为空直接返回
if(StrUtil.isBlank(shopJson)){

return null;
}


//2。2 存在,判断缓存是否过期

//先将json转为实体类
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);

//取出过期时间进行比较
LocalDateTime expireTime = redisData.getExpireTime();

JSONObject data = (JSONObject)redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);

//未过期直接返回,过期时间在现在时间之后
if(expireTime.isAfter(LocalDateTime.now())){
return shop;
}

//过期,获取互斥锁,重建缓存

//获取锁失败,返回过期信息
if (tryLock(RedisConstants.LOCK_SHOP_KEY+id)) {
//获取锁成功,再次检测缓存,是否过期,未过期直接返回,过期再重建

shopJson=stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY+id);

//为空直接返回
if(StrUtil.isBlank(shopJson)){

return null;
}

//2。2 存在,判断缓存是否过期

//先将json转为实体类
redisData = JSONUtil.toBean(shopJson, RedisData.class);

//取出过期时间进行比较
expireTime = redisData.getExpireTime();

data = (JSONObject)redisData.getData();
shop = JSONUtil.toBean(data, Shop.class);

//未过期直接返回,过期时间在现在时间之后
if(expireTime.isAfter(LocalDateTime.now())){
return shop;
}

//过期重建缓存
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
this.saveShopToRedis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
unLock(RedisConstants.LOCK_SHOP_KEY+id);
}
});
}

//获取锁失败,返回过期信息
return shop;
}

获取锁之后,要进行必要的二次检查缓存,减少不必要的缓存重建