【 Redis | 实战篇 缓存 】

#王者杯·14天创作挑战营·第1期#

目录

前言:

1.认识缓存

2.添加Redis缓存

2.1.根据id查询商铺缓存

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

3.缓存更新策略

3.1.三种策略

3.2.策略选择

3.3.主动更新的方案

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要想抢到执行权来进行数据库查询的操作的概率极低

4. 如何保证数据库与缓存操作原子性

  • 单体系统:利用事务机制

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

图:

 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以及泛型函数

评论 97
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值