逻辑过期解决缓存击穿

我先说一下正常的业务流程:需要查询店铺数据,我们会先从redis中查询,判断是否能命中,若命中说明redis中有需要的数据就直接返回;没有命中就需要去mysql数据库查询,在数据库中查到了就返回数据并把该数据存入redis中,若mysql数据库中也查不到就返回null,并返回错误信息:该信息不存在。

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务复杂的存储在redis中的key突然失效,无数请求就会瞬间打到数据库造成巨大冲击。

 解决方法有有俩个,一个是用互斥锁,一个是逻辑过期时间。互斥锁方法的实现写在了另一篇文章中,需要的可以去看一下。

逻辑过期时间:这个是不给存入的key设置过期时间,而是将过期时间写入value中,时间过期后,一个线程获取互斥锁然后另开一个新线程去查询数据库,写入缓存并释放锁。而老线程直接返回查到的旧数据,期间其他获取互斥锁失败的线程查询也会返回旧数据。缺点:有额外的内存消耗,不保证数据一致性,实现优点复杂。

在原来我们只需要把数据库查到的实体类信息保存到redis中就可以了,现在用逻辑过期时间解决缓存击穿,我们还需要把过期时间和实体类信息一起保存到redis中。所以我们需要新写一个包装类把过期时间和实体类封装到一个类中,如下:

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

考虑到该包装类的复用,所以实体类直接传一个Object类,而没有用泛型

所以流程是:去redis查询数据,没命中直接返回null;命中就取出过期时间信息,查看是否过期,若未过期,则直接返回实体类信息;若过期就进行缓存重建,重试获取互斥锁,获取成功就另开启一个新线程去访问数据库进行数据重建,而本线程返回旧数据;若没有获取到锁,就直接返回旧数据。

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;

public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = queryWithPassThrough(id);

        //用互斥锁解决缓存击穿
        /*Shop shop = queryWithMutex(id);
        if (shop==null){
            return Result.fail("店铺不存在");
        }*/
        //用逻辑过期时间解决缓存击穿
        Shop shop = queryWithLoginExpire(id);
        return Result.ok(shop);
    }

private static final ExecutorService CACHE_REBUILD_EXECUTOR =Executors.newFixedThreadPool(10);

    public Shop queryWithLoginExpire(Long id) {
        //1.从redis查询数据缓存
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,返回空值
            return null;
        }
        //4.存在,将json字段反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        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 lock = tryLock(lockKey);
        //6.2判断是否获取锁成功
        if (lock){
            //6.3获取成功,开启新线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
               //重建缓存
                try {
                    saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    UnLock(lockKey);
                }
            });
        }
        //6.3失败,返回旧消息
        return shop;
    }

//下面这是进行访问数据库缓存重建的方法
 public void saveShop2Redis(Long id,Long expireTime) throws InterruptedException {
        //1、获取店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //2、封装逻辑过期时间
        RedisData redisData=new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        //3.保存到缓存中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
    }
}

下面让我们用Jmeter测试一下,看看高并发的状态下,会访问数据库几次:

因为前面代码中,我们写的是在redis中如果找不到就直接返回null了,所以在测试之前我们应该先用测试方法,执行一下saveShop2Redis方法,把数据缓存到redis中。

@Test
    void testSaveShop2Redis() throws InterruptedException {
        shopService.saveShop2Redis(1L,10L);
    }

这样我们就把实体类id为1,过期时间为10s缓存到了redis中了。

 redis中已经有了我们存的数据了,也存日了过期时间,然后等个10s,等过了过期时间,在用Jmeter做高并发测试

 idea控制台返回

 信息可知缓存中逻辑时间过期后就会进行缓存重建,并只会访问数据库一次。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值