基于逻辑过期进行缓存预热,解决缓存击穿问题
大体思路:
所有热点数据会提前存入redis,如果缓存未命中,说明不是热点数据,可以直接返回空
缓存穿透是有大量不存在数据访问redis,redis未命中然后去访问数据库,就造成了缓存穿透。
这里我们只针对的热点数据,操作的是缓存,要判断的也只是数据是否过期,过期就另开一个线程重建缓存,在这个重建缓存的时间内,其他并发线程访问缓存,返回的是过期的数据,这个树据在缓存中并没有真的过期,只是存入了一个过期时间,后面判断就是根据这个过期时间进行的,所以说是逻辑过期,保证了可用性。

实现
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){
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if(StrUtil.isBlank(shopJson)){
return null; }
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; }
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; }
|
获取锁之后,要进行必要的二次检查缓存,减少不必要的缓存重建