【 Redis | 实战篇 缓存 】

目录

前言:

1.认识缓存

2.添加Redis缓存

2.1.根据id查询商铺缓存

2.2.优化根据id查询商铺缓存

3.缓存更新策略

3.1.三种策略

3.2.策略选择

3.3.主动更新的方案

[3.4. Cache Aside的模式选择](#3.4. Cache Aside的模式选择)

3.5.最佳实践方案

4.缓存三大问题

4.1.缓存穿透

4.1.1.介绍

4.1.2.解决方案

4.1.3.实现

4.2.缓存雪崩

4.2.1.介绍

4.2.2.解决方案

4.3.缓存击穿

4.3.1.介绍

4.3.2.解决方案

4.3.3.实现

4.4.封装缓存工具


前言:

了解什么是缓存,怎么缓存,缓存的更新策略,缓存的三大问题及解决方案(缓存穿透,缓存雪崩,缓存击穿)

1.认识缓存

1.1.缓存的介绍

缓存就是数据交换的缓冲区,是储存数据的临时地方( 一种具备高效读写能力的数据暂存区域

1.2.缓存的作用

  • 降低后端负载

  • 提高读写速率,降低响应时间

1.3.缓存的成本

  • 1.开发成本 (代码维护成本)

  • 2.运维成本

  • 3.数据一致性成本

图:

2.添加Redis缓存

2.1.根据id查询商铺缓存

步骤:

前端提交商铺id

==》从Redis中查询缓存

==》判断缓存是否存在(是否命中)

==》命中返回商铺数据

-------------------

==》未命中

==》根据id查询数据库

==》判断数据是否存在

==》不存在返回404,存在将数据写入Redis

==》返回商铺数据

复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate; 
@Override
    public Result queryShopById(Long id) {
        Shop shop = queryShopPenetrate(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }
        //6.返回商铺数据
        return Result.ok(shop);
    }


    public Shop queryShopPenetrate(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
  
        //3.不存在,查询数据库
        Shop shop = getById(id);
        //4.判断是否存在
        if (shop == null) {
            return null;
        }

        //5.存在,存入Redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, jsonStr);
        return shop;
    }

解释:

  1. 1.由于商铺信息一般不进行修改,而用户却需要频繁的访问这些数据,如果突然有大量用户同时访问该数据,那么数据库的压力会很大,因此我们需要增加用户访问速度和降低对数据库的压力,所以我们使用Redis来进行缓存(基于内存,读写速度更快,降低数据库的压力)
  2. 2.用户点击商铺,前端返回对应id,那么后端接收到id在Redis查询(没有数据Redis会返回null),因此我们需要判断其是否命中,缓存存在直接返回缓存数据即可,不存在没有数据,那么我们需要查询数据库,再次判断数据是否存在,没有存在那么就是根本就没有这个商铺的信息直接返回错误信息,数据存在,我们需要先将数据写入Redis以便以后访问再返回数据给前端

2.2.优化根据id查询商铺缓存

步骤:

前端提交商铺id

==》从Redis中查询缓存

==》判断缓存是否存在(是否命中)

==》命中返回商铺数据


==》未命中

==》根据id查询数据库

==》判断数据是否存在

==》不存在返回404,存在将数据写入Redis,并且设置过期时间(过期淘汰)

==》返回商铺数据

复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate; 
@Override
    public Result queryShopById(Long id) {
        Shop shop = queryShopPenetrate(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }
        //6.返回商铺数据
        return Result.ok(shop);
    }


    public Shop queryShopPenetrate(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
        //3.不存在,查询数据库
        Shop shop = getById(id);
        //4.判断是否存在
        if (shop == null) {
            return null;
        }

        //5.存在,存入Redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

解释:为什么要设置过期时间,要保证缓存数据定时更新

3.缓存更新策略

3.1.三种策略

1.内存淘汰:Redis自带的内存淘汰机制,不需要自己维护,当Redis内存不足时会自动的淘汰(清理)部分数据,等下次查询时更新缓存即可


特性:一致性差 ,没有维护成本
2.过期淘汰:给缓存数据添加过期时间(利用expire命令设置),到期自动删除缓存,等下次查询时更新缓存即可


特性:一致性一般,维护成本低
3.主动更新:自己编写业务逻辑,在修改数据库的同时更新缓存(主动完成数据库和缓存的同时更新)


特性:一致性好,维护成本高

图:

3.2.策略选择

要求数据低一致性

  • 内存淘汰或过期淘汰

要求数据高一致性

  • 主动更新为主,过期淘汰兜底

图:

3.3.主动更新的方案

方案一:Cache Aside

介绍:由缓存调用者在更新数据库的同时更新缓存


特性:一致性良好,实现难度一般

方案二:Read/Write Through

介绍:缓存与数据库集成为一个服务,由服务保证两者的一致性,对外暴露API接口 ,调用者调用API即可,无需知道自己操作的是数据库还是缓存,不关心一致性问题


特性:一致性优秀,实现复杂,性能一般

方案三:Write Back

介绍:调用者只操作缓存,由其他线程来异步将缓存数据持久化到数据库,保证最终一致


特性:一致性差,性能好,实现复杂

图:

3.4. Cache Aside的模式选择

1.该模式就是开发人员手动进行数据库与缓存的代码实现

2.思考更新缓存还是删除缓存:当数据库内的数据发生改变时,那么Redis缓存是不是也需要修改(保存数据一致性),那么我们是去更新缓存,还是直接删除缓存,等要使用该数据时(此时缓存无数据,查询数据库再写入)才进行写入缓存

更新缓存:是不是每次更新数据库时都需要进行更新缓存(无效操作较大且复杂),存在较大的线程安全问题


解释:在一个极短的时间内数据库进行了多次的更新操作,那么缓存是不是也需要进行相同次操作,但其实数据库最后一次修改时缓存更新才是有效的
删除缓存:删除缓存的本质就是延迟更新,没有无效更新,线程安全问题相对较低


解释: 在一个极短的时间内数据库进行了多次的更新操作,而缓存在第一次更新操作时就进行了删除缓存,不管后面有多少次更新操作都影响不到缓存,一直等到用户点击,查询数据库时(用到数据时)才会进行缓存更新

3.思考在写操作时是先操作数据库还是缓存

先删除缓存,再更新数据库 :安全问题概率高


解释:

前提:假设数据库与Redis现在存的数据是100


反例:当数据库进行更新时,将数据100更新为120而在更新的同时进行了查询操作

==》线程1先执行

==》线程1删除缓存(100)

==》线程2抢到执行权

==》线程2执行查询数据操作

==》线程2查询缓存没有数据(无)

==》线程2查询数据库(100)

==》线程2再将数据写入Redis缓存中(100)

==》线程2执行完,线程1执行

==》线程1更新数据库(120)


那么下次查询数据时由于缓存有数据,并不会更新缓存,我们发现缓存数据为100,数据库数据为120,数据不一致

先更新数据,再删除缓存: 在满足原子性的情况下,安全问题较低


解释:(也有反例,不过概率很低)

前提:假设数据库存的数据是100,Redis没有存数据


反例:在查询数据库的同时进行了更新数据库操作将100更新为120

==》线程1先执行

==》线程1查询缓存(无),不存在

==》线程1查询数据库(100)

==》线程2抢到执行权

==》线程2更新数据库(120)

==》线程2删除缓存

==》线程2执行完,线程1执行

==》线程1将数据100写入缓存(100)


那么下次查询数据时由于缓存有数据,并不会更新缓存,我们发现缓存数据为100,数据库数据为120,数据依旧不一致


注意:为什么这种概率极低呢,因为缓存的读写是基于内存的,而数据库读写基于硬盘,缓存的操作远远快于数据库操作,因此在线程1写入缓存之前,线程2要想抢到执行权来进行数据库查询的操作的概率极低

  1. 如何保证数据库与缓存操作原子性
  • 单体系统:利用事务机制

  • 分布式系统:利用分布式事务机制

图:

3.5.最佳实践方案

1.低一致性需求:使用Redis自带的内存淘汰机制

2.高一致性需求:主动更新,并以超时剔除作为兜底方案

读操作:

  • 缓存命中直接返回
  • 没命中查询数据库,并写入缓存,设置超时时间

例子:

前端提交商铺id

==》从Redis中查询缓存

==》判断缓存是否存在(是否命中)

==》命中返回商铺数据


==》未命中

==》根据id查询数据库

==》判断数据是否存在

==》不存在返回404,存在将数据写入Redis,并且设置过期时间(过期淘汰)

==》返回商铺数据

复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate; 
@Override
    public Result queryShopById(Long id) {
        Shop shop = queryShopPenetrate(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }
        //6.返回商铺数据
        return Result.ok(shop);
    }


    public Shop queryShopPenetrate(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
  
        //3.不存在,查询数据库
        Shop shop = getById(id);
        //4.判断是否存在
        if (shop == null) {
            return null;
        }

        //5.存在,存入Redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

写操作:

  • 先写数据库,然后再删除缓存
  • 确保数据库与缓存操作的原子性

例子:

复制代码
 @Override
    @Transactional
    public Result updateShop(Shop shop) {
        //1.判断商铺是否存在
        Long id = shop.getId();
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        if (id == null) {
            return Result.fail("商铺不存在");
        }

        //2.先更新数据库
        updateById(shop);

        //3.删除Redis
        stringRedisTemplate.delete(key);
        return Result.ok();
    }

图:

4.缓存三大问题

4.1.缓存穿透

4.1.1.介绍

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求最终都会打到数据库中

例子**:数据库和Redis缓存中都没有数据,但是用户一直频繁访问发出请求,导致大量请求直接打到数据库上,导致数据库崩塌**

4.1.2.解决方案

方案一:缓存空对象

  • 思路:对不存在的数据也在Redis中建立缓存值,值为空,并且设置一个较短的时间
  • 优点:实现简单,维护方便
  • 缺点:有额外的内存消耗,短期的数据不一致问题

解释:为什么要设置一个有过期时间的缓存空值,不是用户频繁请求 吗,那么我们就给它一个值,防止压力数据库,不过这样会造成数据不一致问题,就是当数据设置空值后,正好数据库添加了相应的数据,那么此时数据将不一致(不过由于我们设置的是较短的过期时间,所以数据不一致时间存在时间不会太久),由于你设置了空值(不必要值),那么会造成内存的消耗

方案二:布隆过滤

  • 思路:利用布隆过滤算法,在请求进入Redis之前先判断是否存在,如果不存在则直接拒绝请求
  • 优点:内存占用少
  • 缺点:实现复杂,存在误判的可能性

解释:本质就是将数据库,Redis中的数据基于一种哈希算法计算出哈希值,再转化成二进制,最终存入过滤器中(1就是存在值,0就是不存在值)

注意**:基于哈希算法,那么就会出现哈希冲突问题,导致过滤器判断存在数据可能数据库/Redis中并没有数据(不存在数据就一定不存在,存在有可能不存在)**

方案三:细节

  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

4.1.3.实现

步骤:

前端提交商铺id

==》从Redis中查询缓存

==》判断缓存是否存在(是否命中)

==》命中

==》判断数据是否为空值

==》空值直接返回错误信息,不为空返回商铺数据


==》未命中

==》根据id查询数据库

==》判断数据是否存在

==》不存在将空值(设置过期时间)存入Redis,存在将数据写入Redis,并且设置过期时间(过期淘汰)

==》返回商铺数据

复制代码
 @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryShopById(Long id) {
        //缓存穿透
        Shop shop = queryShopPenetrate(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }
        //6.返回商铺数据
        return Result.ok(shop);
    }

    //穿透
    public Shop queryShopPenetrate(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
        if (strShop != null) {
            return null;
        }
        //3.不存在,查询数据库
        Shop shop = getById(id);
        //4.判断是否存在
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //5.存在,存入Redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

图:

4.2.缓存雪崩

4.2.1.介绍

缓存雪崩是在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

4.2.2.解决方案

  • 给不同的Key的过期时间添加随机值

  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

解释:

给不同的Key的过期时间添加随机值:避免key同时失效

利用Redis集群提高服务的可用性:利用集群,主从,哨兵机制(主机宕机,从来代主实现并且从与主的数据一致)

给缓存业务添加降级限流策略:当整个机房都挂了(Redis都掉了),出现了超大故障时,直接返回拒绝服务,避免请求压力到数据库

给业务添加多级缓存:1.浏览器缓存静态数据 2.nginx缓存数据 3.jvm内部本地缓存 4.Redis缓存 5.数据库储存

图:

4.3.缓存击穿

4.3.1.介绍

缓存击穿就是热点key问题:就是一个被高并发访问 (访问频率高)并且缓存重建业务较复杂( 查询数据库业务复杂,耗时长)的key突然失效了 ,那么无数的请求访问会在一瞬间给数据库带来巨大冲击

4.3.2.解决方案

方案一:互斥锁

  • 思路:给缓存重建过程加锁,确保重建过程只有一个线程执行,其他线程等待它执行完成
  • 优点:实现简单,没有额外的内存消耗,一致性好
  • 缺点:等待导致性能下降,有死锁风险

解释:基于Redis中的命令setnx来实现锁,由于setnx命令是key有值就不赋值,没有才创建key并且赋值 ,利用这个特性实现自定义锁(只有第一个人可以成功写入数据,其他人就不能),而由于多个线程同时访问时都需要等待(如果重建时间久)那么性能将会减低

方案二:逻辑过期

  • 思路:热点key缓存永不过期,而是设置一个逻辑过期时间,查询到数据时通过对逻辑过期时间判断,来决定是否需要重建缓存
  • 优点:线程无需等待,性能较好
  • 缺点:不保证一致性,有额外内存消耗,实现复杂

解释:由于是热点key那么在一段时间(活动时间内),key应该不会去修改(活动之前就会缓存好key),那么我们也不需要进行key的自动删除(设置真正的过期时间),设置逻辑时间,**根据实际时间与逻辑时间对比,那么我们就可以知道key是否过期,**来进行对应操作

4.3.3.实现

方案一:互斥锁

步骤:

前端提交商铺id

==》线程1从Redis中查询缓存

==》线程1判断缓存是否存在(是否命中)

==》命中

==》线程1判断数据是否为空值

==》空值直接返回错误信息,不为空返回商铺数据


==》未命中

==》线程1尝试获取互斥锁

==》线程1判断是否获取到锁

==》线程1获取到锁

==》线程1再次检查缓存是否存在

==》缓存存在直接返回缓存,不存在查询

==》线程1根据id查询数据库

==》线程1判断数据是否存在

==》线程1不存在将空值(设置过期时间)存入Redis,存在将数据写入Redis,并且设置过期时间(过期淘汰)

==》线程1释放锁

==》线程1返回商铺数据


==》线程2在线程1还未释放锁时也执行查询操作

==》线程2尝试获取锁

==》线程2判断是否获取到锁

==》线程2未获取到锁

==》线程2休眠一段时间并且返回到查询Redis缓存操作阶段

复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate;
 
 @Override
    public Result queryShopById(Long id) {
        
        //互斥锁缓存击穿
       Shop shop = queryShopBreakdown(id);
       if (shop == null){
           return Result.fail("商铺不存在");
        }
       
        //返回商铺数据
        return Result.ok(shop);
    }
 
//基于互斥锁,击穿
    public Shop queryShopBreakdown(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
        if (strShop != null) {
            return null;
        }
        //获取锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            Boolean lock = lock(lockKey);
            if(!lock){
                //获取锁失败,递归
                Thread.sleep(50);
                return queryShopBreakdown(id);
            }
            //获取锁,再次查询缓存
            strShop = stringRedisTemplate.opsForValue().get(key);
            //判断缓存是否存在
             if (StrUtil.isNotBlank(strShop)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(strShop, Shop.class);
            return shop;
        }
            if (strShop != null) {
            return null;
        }
            //3.不存在,查询数据库
            shop = getById(id);
            //4.判断是否存在
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            //5.存在,存入Redis
            String jsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //移除锁
            removeLock(lockKey);
        }
        return shop;
    }

 //获取锁
    public Boolean lock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    public void removeLock(String key) {
        stringRedisTemplate.delete(key);
    }

解释:就是当第一个线程获取到锁后并且还没有释放锁,而其本质就是利用命令setnx来建立key赋值并且设置过期时间,在没有线程获取到锁时(没有线程赋值key)那么此时setnx命令是可以执行成功的,**执行成功返回对应数字(成功返回1,不成功返回0)**根据数字判断是否成功赋值从而判断是否获取到锁。

那么其他线程获取不到锁那就说明锁未释放(删除key),线程就一直等待直到第一个线程释放锁

注意:我们在删除锁时(没有删除)或者是程序出错了,导致锁没有释放,那么就会出现死锁,因此我们预估业务执行时间,给锁设置一个过期时间防止出现该问题

当线程拿到锁时,我们还需要查询Redis来判断缓存是否存在,可能会出现在线程拿到锁之前正好有一个线程刚好释放了锁(已经完成了写入缓存的操作),那么为了效率我们要再次判断缓存是否存在

方案二:逻辑过期

步骤:

前端提交商铺id

==》线程1从Redis中查询缓存

==》线程1判断缓存是否存在(是否命中)

==》未命中

==》直接返回空值


==》命中

==》线程1判断缓存是否过期(逻辑时间)

==》过期

==》线程1尝试获取互斥锁

==》线程1判断是否获取到锁

==》线程1获取到锁

==》线程1开启新线程2

==》线程1直接返回旧商铺数据


==》线程2再次检查缓存是否过期

==》缓存没有过期直接返回缓存,过期查询

==》线程2根据id查询数据库

==》线程2判断数据是否存在

==》线程2不存在将空值(设置过期时间)存入Redis,存在将数据(设置逻辑过期时间)写入Redis

==》线程2释放锁


==》线程1未获取到锁

==》线程1直接返回旧商铺数据

​​​​​​​

复制代码
 @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

@Override
    public Result queryShopById(Long id) {
       
        //逻辑

        Shop shop = queryExpireTime(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }
        //返回商铺数据
        return Result.ok(shop);
    }

    //逻辑
    public Shop queryExpireTime(Long id) {
        //1.查询Redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String strShop = stringRedisTemplate.opsForValue().get(key);//一定存在
        //2.判断是否存在
        if (StrUtil.isBlank(strShop)) {
            //不存在直接返回
            return null;
        }

        //3.存在,判断过期时间
        RedisData redisData = JSONUtil.toBean(strShop, 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;
        }
        //4.过期
        //获取锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Boolean lock = lock(lockKey);
        if(lock){
            //获取锁
            //再次判断缓存是否过期
        strShop = stringRedisTemplate.opsForValue().get(key);//一定存在
        //判断缓存是否存在
        if (StrUtil.isBlank(strShop)) {
            //不存在直接返回
            return null;
        }

        //存在,判断过期时间
        RedisData redisData = JSONUtil.toBean(strShop, 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;
        }
            //过期,开启线程
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    this.expireTime(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    removeLock(lockKey);
                }
            });
        }
        //没有获取锁
        return shop;
    }
    //存入逻辑Redis
    public void expireTime(Long id,Long expire){
        //根据id查询数据库
        Shop shop = getById(id);
        //存入Redis
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expire));
        redisData.setData(shop);
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
    }

解释:由于是热点key问题(key不会过期),你想一般在活动开始之前这些key是不是就需要准备好(已经缓存好了),所以说明什么,key一定是存在的(不存在,那么该key不是属于该活动返回空值就行),那么我们可以将之前设置给key的过期时间改为逻辑时间(key在活动时间内一定存在,逻辑时间就是活动时间),我们之后只需要判断活动是否已经结束就行(将逻辑时间与实际时间对比),未过期直接返回数据

过期,线程1获取锁,没有获取到说明已经有线程在执行,那么线程1也不需要等待直接返回一个旧的数据(只要锁没有释放,其他线程无需等待直接返回旧的数据),获取到锁,线程1开启一个新的线程2来执行重建缓存操作,而线程1还是直接返回旧的数据

注意:获取到锁成功后还需要判断Redis缓存是否过期,可能在线程拿到锁之前正好有另外一个线程刚好重建了缓存(更新了逻辑时间),那么我们需要再次判断避免重复构建

细节:由于之前实体类你没有单独设置一个逻辑时间属性,那么此时你需要用到该属性该怎么办

方法一:创建一个新的实体类写入时间属性,让原先实体类来继承

缺点:修改了原先实体类数据,并且以后每次需要实现逻辑时间属性时你都需要继承该类,过于繁琐
方法二:创建一个新实体类,写入时间属性并且写入Object类型属性,将原先的实体类数据封装到Object中即可

优点:实现了复用性,不需要修改原先实体类数据

总结:组合优先于继承

图:

4.4.封装缓存工具

实现:

复制代码
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class CacheUtils {

    //注入
    private final StringRedisTemplate stringRedisTemplate;

    public CacheUtils(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    //穿透,写入Redis
    private void set(Long time, TimeUnit unit, String key, Object value) {
        String jsonStr = JSONUtil.toJsonStr(value);
        stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);
    }
    //击穿,写入Redis
    private void setTime(Long time, TimeUnit unit, String key, Object value) {
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }
    //穿透
    public <R,ID> R queryPenetrate(String keyPrefix, ID id, Class<R> type, Function<ID,R> function,Long time,TimeUnit unit) {
        //1.查询Redis
        String key = keyPrefix + id;
        String JSON = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(JSON)) {
            //存在直接返回
            return JSONUtil.toBean(JSON, type);
        }
        if (JSON != null) {
            return null;
        }
        //3.不存在,查询数据库
        R r = function.apply(id);
        //4.判断是否存在
        if (r == null) {
            set(RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES, key, "");
            return null;
        }
        //5.存在,存入Redis
        this.set(time, unit, key, r);
        return r;
    }



    //逻辑击穿
    public <R,ID> R queryExpireTime(String keyPrefix, String lockPrefix,ID id, Class<R> type, Function<ID,R> function,Long time,TimeUnit unit) {
        //1.查询Redis
        String key = keyPrefix + id;
        String JSON = stringRedisTemplate.opsForValue().get(key);//一定存在
        //2.判断是否存在
        if (StrUtil.isBlank(JSON)) {
            //不存在直接返回
            return null;
        }
        //3.存在,判断过期时间
        RedisData redisData = JSONUtil.toBean(JSON, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        if(expireTime.isAfter(LocalDateTime.now())){
            //没有过期,直接返回
            return r;
        }
        //4.过期
        //获取锁
        String lockKey = lockPrefix + id;
        Boolean lock = lock(lockKey);
        if(lock){
            //获取锁
            //开启线程
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    //根据id查询数据库
                    R r1 = function.apply(id);
                    //存入Redis
                   this.setTime(time,unit,key,r1);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    removeLock(lockKey);
                }
            });
        }
        //没有获取锁
        return r;
    }

    //获取锁
    public Boolean lock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    public void removeLock(String key) {
        stringRedisTemplate.delete(key);
    }
}

解释:由于是封装工具,那么我们需要做到多样性,方法传参时不能定义死,采用泛型来实现复用性,由于使用的是mybatis-plus工具(需要查询数据库)而我们的实体类不能确定,因此需要传参Class以及泛型函数

相关推荐
Navicat中国15 分钟前
如何使用 Ollama 配置 AI 助手 | Navicat 教程
数据库·人工智能·ai·navicat·ollama
小猿姐5 小时前
实测对比:哪款开源 Kubernetes MySQL Operator 最值得用?(2026 深度评测)
数据库·mysql·云原生
倔强的石头_7 小时前
从 “存得下” 到 “算得快”:工业物联网需要新一代时序数据平台
数据库
TDengine (老段)8 小时前
TDengine IDMP 可视化 —— 分享
大数据·数据库·人工智能·时序数据库·tdengine·涛思数据·时序数据
布局呆星8 小时前
SpringBoot 基础入门
java·spring boot·spring
风吹迎面入袖凉8 小时前
【Redis】Redisson的可重入锁原理
java·redis
GottdesKrieges8 小时前
OceanBase数据库备份配置
数据库·oceanbase
SPC的存折9 小时前
MySQL 8组复制完全指南
linux·运维·服务器·数据库·mysql
运维行者_9 小时前
OpManager MSP NetFlow Analyzer集成解决方案,应对多客户端网络流量监控挑战
大数据·运维·服务器·网络·数据库·自动化·运维开发
炸炸鱼.10 小时前
Python 操作 MySQL 数据库
android·数据库·python·adb