当前位置:   article > 正文

黑马redis实战篇-商铺缓存_redis实战篇--商户查询缓存

redis实战篇--商户查询缓存

目录

五、实战篇-商户查询缓存

5.1 什么是缓存

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

2、添加redis时,数据查询的作用模型:

3、业务流程图:​编辑

4、代码实现

5、练习题

5.3 缓存更新策略

1、主动更新

2.Cache Aside Pattern(旁路缓存模式)

3、总结

4、给查询商铺的缓存添加超时剔除和主动更新的策略

5.4 缓存穿透

1、解决方案

2、解决商铺查询时,缓存穿透问题

3、总结

5.5 缓存雪崩

5.6 缓存击穿

1、解决方案

2、基于互斥锁方式解决缓存击穿问题

3、基于逻辑过期方式解决缓存击穿问题

4、JMeter下载和安装

5.7 缓存工具封装


五、实战篇-商户查询缓存

5.1 什么是缓存

缓存就是数据交换的缓冲区(称作Cache ),是存储数据的临时地方,一般读写性能较高

 

缓存的作用:

  • 降低后端负载 ---直接访问缓存,返回数据

  • 提高读写效率,降低响应时间---基于内存存储

缓存的成本:

  • 数据一致性成本

  • 代码维护成本----解决一致性问题代码复杂

  • 运维成本-- 要保证高可用搭建集群

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

 

2、添加redis时,数据查询的作用模型:

redis命中直接返回数据,未命中数据库查询返回数据,并且将数据缓存到redis中

 

3、业务流程图:

 

4、代码实现

  1. public Result selectShopInfoById(Long id) {
  2.        String key = CACHE_SHOP_KEY + id;
  3.        //1.判断redis中是否存在该id的数据
  4.        String str = stringRedisTemplate.opsForValue().get(key);
  5.        if (StrUtil.isNotBlank(str)) {
  6.            //2.存在 直接返回数据
  7.            return Result.ok(JSONUtil.toBean(str,Shop.class));
  8.       }
  9.        //3.不存在 查询数据库是否存在
  10.        Shop shop = baseMapper.selectById(id);
  11.        if (StringUtils.isEmpty(shop)) {
  12.            //4.不存在直接返回 404
  13.            return Result.fail("店铺不存在");
  14.       }
  15.        //5.存在将数据存储在redis,然后返回
  16.        String shopJsonStr = JSONUtil.toJsonStr(shop);
  17.        stringRedisTemplate.opsForValue().set(key,shopJsonStr);
  18.        return Result.ok(shop);
  19.   }

5、练习题

给店铺类型业务添加缓存

 

  1. public Result queryOrderByAscList() {
  2.        //1.判断redis是否存在 商铺类型的缓存
  3.        Long size = stringRedisTemplate.opsForList().size(CACHE_SHOP_TYPE_KEY);
  4.        if (size > 0){
  5.            //2.存在 直接取出返回
  6.            List<String> range = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, size);
  7.            List<ShopType> shopTypes = range.stream().map(item -> {
  8.                return  JSONUtil.toBean(item,ShopType.class);
  9.           }).collect(Collectors.toList());
  10.            //.sorted(Comparator.comparing(ShopType::getSort).reversed())
  11.            return Result.ok(shopTypes);
  12.       }
  13.        //3.不存在,查询数据库
  14.        QueryWrapper<ShopType> queryWrapper = new QueryWrapper<>();
  15.        queryWrapper.orderByAsc("sort");
  16.        List<ShopType> shopTypes = baseMapper.selectList(queryWrapper);
  17.        if (shopTypes.size() <= 0){
  18.            //4.数据库不存在 直接返回错误
  19.            return Result.fail("数据不存在");
  20.       }
  21.        //5.数据库存在,将数据存储在缓存中然后返回
  22.        List<String> collect = shopTypes.stream().map(item -> {
  23.            return JSONUtil.toJsonStr(item);
  24.       }).collect(Collectors.toList());
  25.        stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY,collect);
  26.        //设置过期时间是1天
  27.        stringRedisTemplate.expire(CACHE_SHOP_TYPE_KEY,CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);
  28.        return Result.ok(shopTypes);
  29.   }

5.3 缓存更新策略

内存淘汰超时剔除主动更新
说明不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存数据添加TTL过期时间,到期后自动删除缓存。下次查询时更新缓存编写业务逻辑,在修改数据的同时,更新缓存
一致性一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存

  • 高一致性需求:主动更新,并以超时时间作为兜底方案。例如店铺详情查询的缓存

1、主动更新

  • Cache Aside Pattern 有缓存的调用缓存,在更新数据库的同时更新缓存

  • Read/Write Through Pattern 缓存和数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无序关心缓存一致性问题

  • Write Behind Caching Pattern 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致

Redis的主动更新有三种常见的方案,包括:
​
Cache Aside Pattern(旁路缓存模式):应用程序先从缓存中获取数据,如果缓存中不存在要访问的数据,则从数据库获取,再将数据写入缓存中。
优点:高效性能,减少数据库访问次数和负载,适合于对数据实时性要求不高的应用。
​
缺点:存在缓存和数据库数据不一致的问题,当读写并发量大时,可能会出现脏数据。
​
Read/Write Through Pattern(读写穿透模式):数据缓存和数据库相连,应用程序从缓存中获取数据,如缓存中没有相应数据,会通过缓存访问层查找数据。该层在未命中数据后,查询数据库。若命中则返回数据,并同步写入缓存中;否则返回空值或默认值。
优点:保证缓存、数据库数据一致性,并且在缓存失效的情况下,也可以避免因读操作而引起的数据库压力过大,同时也可以防止缓存数据与数据库之间的数据不一致。
​
缺点:每次访问数据都必须通过缓存去访问数据库,增加了结构的复杂性并降低了系统的效率。
​
Write Behind Caching Pattern(写回缓存模式):在进行写操作时,不直接将数据写入到数据库中,而是先将数据写入缓存中,待缓存达到一定条件后再批量同步到数据库中。
优点:提高了写操作的性能,并且降低了数据库负载,可以适用于写入比较频繁但读取全量较少的应用场景,同时也减少了与数据库的交互次数和延迟。
​
缺点:由于只在达到缓存阈值之后才进行同步,因此可能会存在缓存中未及时更新的数据,从而引起数据不一致性问题,同时当缓存重新启动时还需要从磁盘上读取数据进行恢复,增加了复杂度。
​
需要针对具体应用场景选择合适的主动更新方案,并结合Redis中提供的其他功能一起使用。

这里比较常用的:Cache Aside Pattern(旁路缓存模式)

2.Cache Aside Pattern(旁路缓存模式)

操作缓存和数据库有三个问题需要考虑:

  1. 删除缓存还是更新缓存?

    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多

    • 删除缓存:更新数据库时让缓存失败,查询时再更新缓存(比较符合)

  2. 如何保障缓存与数据库的操作同时成功或失败?

    • 单体系统,将缓存与数据库操作放在一个事务

    • 分布式系统,利用TCC等分布式事务方案

  3. 先操作缓存还是先操作数据库?

    • 先删除缓存,再操作数据库(不推荐,更新数据库时间长,出现概率很大)

      第一个线程删除缓存后,在更新数据库的时候,还没更新成功的时候,
      ​
      第二个线程访问了,发现缓存没有,查询数据库的数据,这是数据库的数据的旧的,将旧的数据更新到缓存中出现了不一致性

       

      可以使用延时双删的策略,即先删除缓存,在更新数据库,然后休眠500毫秒在删除缓存,但是因为第二次延时时间,不确定性很大,一般不推荐使用

    • 先操作数据库,再删除缓存(推荐,相较于上一种出现概率很低)

      因为某种原因,缓存找中数据没了,线程1访问的时候发现没有缓存,查询数据库得到旧数据,要进行写入缓存操作时
      线程2进行了更新数据库,删除缓存,然后线程1更新了缓存为旧数据

       

3、总结

缓存更新策略的最佳实践方案:

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

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

    • 读操作 Cache Aside Pattern(旁路缓存模式):

      • 缓存未命则直接返回

      • 缓存未命中则查询数据库,并写入缓存,设定超时时间

    • 写操作:

      • 先写数据库,然后再删除缓存

      • 要确保数据库与缓存操作的原子性

4、给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:

  1. 根据id查询商铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

    1. @Override
    2.    public Result selectShopInfoById(Long id) {
    3.        String key = CACHE_SHOP_KEY + id;
    4.        //1.判断redis中是否存在该id的数据
    5.        String str = stringRedisTemplate.opsForValue().get(key);
    6.        if (StrUtil.isNotBlank(str)) {
    7.            //2.存在 直接返回数据
    8.            return Result.ok(JSONUtil.toBean(str,Shop.class));
    9.       }
    10.        //3.不存在 查询数据库是否存在
    11.        Shop shop = baseMapper.selectById(id);
    12.        if (StringUtils.isEmpty(shop)) {
    13.            //4.不存在直接返回 404
    14.            return Result.fail("店铺不存在");
    15.       }
    16.        //5.存在将数据存储在redis,然后返回
    17.        String shopJsonStr = JSONUtil.toJsonStr(shop);
    18.        stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
    19.        return Result.ok(shop);
    20.   }

  2. 根据id修改店铺时,先修改数据库,再删除缓存

    1. @Override
    2.    @Transactional
    3.    public Result updateShopById(Shop shop) {
    4.        Long id = shop.getId();
    5.        if (id == null) {
    6.            return Result.fail("店铺id不能为空");
    7.       }
    8.        //修改数据库
    9.        baseMapper.updateById(shop);
    10.        //删除缓存
    11.        stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
    12.        return Result.ok();
    13.   }

5.4 缓存穿透

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

1、解决方案

常见的解决方案有两种:

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:

      • 额外的内存消耗 ---设置ttl过期时间

      • 可能造成短期的不一致 ----插入数据的时候,更新缓存将null的覆盖

         

  • 布隆过滤

    • 优点:内存占用较少,没有多余key

    • 缺点:

      • 实现复杂

      • 存在误判可能---布隆过滤器是居于hash算法,存在哈希碰撞问题

        判断不存在的肯定不存在,判断存在的时候,可能不存在

         

2、解决商铺查询时,缓存穿透问题

 

  1. public Result selectShopInfoById(Long id) {
  2.        String key = CACHE_SHOP_KEY + id;
  3.        //1.判断redis中是否存在该id的数据
  4.        String str = stringRedisTemplate.opsForValue().get(key);
  5.        if (StrUtil.isNotBlank(str)) {
  6.            //2.存在 直接返回数据
  7.            return Result.ok(JSONUtil.toBean(str,Shop.class));
  8.       }
  9.        //上面判断后 执行到这句的时候,只能是null或者空字符串
  10.        if (str != null) {
  11.            return Result.fail("店铺不存在");
  12.       }
  13.        //3.不存在 查询数据库是否存在
  14.        Shop shop = baseMapper.selectById(id);
  15.        if (StringUtils.isEmpty(shop)) {
  16.            //将null存入到redis中
  17.            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
  18.            //4.不存在直接返回 404
  19.             return Result.fail("店铺不存在");
  20.       }
  21.        //5.存在将数据存储在redis,然后返回
  22.        String shopJsonStr = JSONUtil.toJsonStr(shop);
  23.        stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
  24.        return Result.ok(shop);
  25.   }

3、总结

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存和数据库汇总都不存在,不断发起这样的请求给数据库带来巨大压力

缓存穿透的解决方案有那些?

  • 缓存null值

  • 布隆过滤器

  • 增强id的复杂度,避免被猜测id规律,然后做好数据的基础格式校验

  • 加强用户权限校验

  • 做好热点参数的限流

5.5 缓存雪崩

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

解决方案:

  • 给不同的key的TTL添加随机值

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

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

  • 给业务添加多级缓存

     

5.6 缓存击穿

缓存击穿问题也叫作热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击。

 

1、解决方案

互斥锁

 

逻辑过期

 

比较

解决方案优点缺点
互斥锁没有额外的内存消耗 保证了一致性 实现简单线程需要等待,性能受影响 可能有死锁的情况
逻辑过期线程无序等待,性能好不保证一致性 存在内存消耗 实现复杂

2、基于互斥锁方式解决缓存击穿问题

需求:根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

 

  1. private  Result cacheShopWithMutex(Long id) {
  2.            String key = CACHE_SHOP_KEY + id;
  3.            Shop shop = null;
  4.            //1.判断redis中是否存在该id的数据
  5.            String str = stringRedisTemplate.opsForValue().get(key);
  6.            if (StrUtil.isNotBlank(str)) {
  7.                //2.存在 直接返回数据
  8.                return Result.ok(JSONUtil.toBean(str,Shop.class));
  9.           }
  10.            //上面判断后 执行到这句的时候,只能是null或者空字符串
  11.            if (str != null) {
  12.                return Result.fail("店铺不存在");
  13.           }
  14.        String lockKey = LOCK_SHOP_KEY + id;
  15.        try {
  16.            //3.不存在 先尝试获取互斥锁 利用redis中string字符串中set
  17.            Boolean flagBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
  18.            boolean flag = BooleanUtil.isTrue(flagBoolean);
  19.            //4.获取锁失败
  20.            if (!flag) {
  21.                //获取锁失败休眠一会
  22.                Thread.sleep(100);
  23.                //然后进行重试 ---递归
  24.                return selectShopInfoById(id);
  25.           }
  26.            //5.如果获取锁成功 查询数据库
  27.            shop = baseMapper.selectById(id);
  28.            //模拟重建延迟
  29.            Thread.sleep(200);
  30.            //如果数据库中没有数据
  31.            if (StringUtils.isEmpty(shop)) {
  32.                //将null存入到redis中
  33.                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
  34.                //不存在直接返回 404
  35.                return Result.fail("店铺不存在");
  36.           }
  37.            //.存在将数据存储在redis,然后返回
  38.            String shopJsonStr = JSONUtil.toJsonStr(shop);
  39.            stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
  40.       } catch (InterruptedException e) {
  41.            e.printStackTrace();
  42.       }finally {
  43.            //6.释放锁
  44.            stringRedisTemplate.delete(lockKey);
  45.       }
  46.        return Result.ok(shop);
  47.   }

3、基于逻辑过期方式解决缓存击穿问题

需求:根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

 

  1. //弄一个线程池
  2.    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  3.    //逻辑过期
  4.    private Result cacheShopWithLogicTTL(Long id) {
  5.        String key = CACHE_SHOP_KEY + id;
  6.        //1.判断redis中是否存在该id的数据
  7.        String str = stringRedisTemplate.opsForValue().get(key);
  8.        if (StrUtil.isBlank(str)) {
  9.            //2.不存在 直接返回空
  10.            return Result.fail("商铺信息为空");
  11.       }
  12.        //3.存在 判断缓存是否过期 逻辑时间
  13.        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
  14.        LocalDateTime expireTime = redisData.getExpireTime();
  15.        JSONObject data = (JSONObject)redisData.getData();
  16.        Shop shop = JSONUtil.toBean(data, Shop.class);
  17.        if (expireTime.isAfter(LocalDateTime.now())) {
  18.            //4.未过期 直接返回商铺信息
  19.            return Result.ok(shop);
  20.       }
  21.        //5.过期了尝试获取互斥锁
  22.        String lockKey = LOCK_SHOP_KEY + id;
  23.        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
  24.        boolean flag = BooleanUtil.isTrue(aBoolean);
  25.        if (!flag) {
  26.            //6.如果未获取到锁 直接返回旧数据
  27.            return Result.ok(shop);
  28.       }
  29.        //7.成功获取到锁 开启一个独立线程
  30.        CACHE_REBUILD_EXECUTOR.submit(() -> {
  31.            //重构缓存
  32.            try {
  33.                saveShopToRedis(id,30L);
  34.           } catch (InterruptedException e) {
  35.                e.printStackTrace();
  36.           }finally {
  37.                stringRedisTemplate.delete(lockKey);
  38.           }
  39.            //释放锁
  40.       });
  41.        //8.返回旧的数据
  42.        return Result.ok(shop);
  43.   }

4、JMeter下载和安装

参考

JMeter下载和安装_仰望_1的博客-CSDN博客

1.下载

2.解压

 

 

3.设置环境变量

 

4.path中设置

 

5.启动

双击打开bin中的jemter.bat

 

就自动启动了

 

6.设置中文

 

7.进行配置

 

 

 

8、输入参数,测试

 

 

 

5.7 缓存工具封装

基于StingRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在String类型的key中,并且可设置TTL过期时间

方法2:将任意Java对象序列化为json并存储在String类型的key中,并在可以设计逻辑过期时间,用于处理缓存击穿问题

方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

封装类:CacheClient

  1. package com.hmdp.utils;
  2. import cn.hutool.core.util.BooleanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import cn.hutool.json.JSONObject;
  5. import cn.hutool.json.JSONUtil;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.data.redis.core.StringRedisTemplate;
  8. import org.springframework.stereotype.Component;
  9. import org.springframework.util.StringUtils;
  10. import javax.annotation.Resource;
  11. import java.time.LocalDateTime;
  12. import java.util.concurrent.ExecutorService;
  13. import java.util.concurrent.Executors;
  14. import java.util.concurrent.TimeUnit;
  15. import java.util.function.Function;
  16. import static com.hmdp.utils.RedisConstants.*;
  17. /**
  18. * @packageName: com.hmdp.utils
  19. * @author: winter
  20. * @date: 2023/4/25 8:55
  21. * @version: 1.0
  22. * @email 1660420659@qq.com
  23. * @description: 封装Redis工具类
  24. */
  25. @Slf4j
  26. @Component
  27. public class CacheClient {
  28.    @Resource
  29.    private StringRedisTemplate stringRedisTemplate;
  30.    /**
  31.     * 将任意Java对象序列化为json并存储在String类型的key中,
  32.     * 并且可设置TTL过期时间
  33.     * @param key key
  34.     * @param obj 存储对象
  35.     * @param timeTTL 过期时间
  36.     * @param timeUnit 单位
  37.     */
  38.    public void set(String key, Object obj, Long timeTTL, TimeUnit timeUnit) {
  39.        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj),timeTTL,timeUnit);
  40.   }
  41.    /**
  42.     * 将任意Java对象序列化为json并存储在String类型的key中,
  43.     * 并在可以设计逻辑过期时间,用于处理缓存击穿问题
  44.     * @param key key
  45.     * @param obj 存储对象
  46.     * @param timeTTL 过期时间
  47.     * @param timeUnit 单位
  48.     */
  49.    public  void setWithLogicalExpire(String key,Object obj,Long timeTTL, TimeUnit timeUnit) {
  50.        RedisData redisData = new RedisData();
  51.        redisData.setData(obj);
  52.        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeTTL)));
  53.        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
  54.   }
  55.    /**
  56.     * 通过key获取字符串
  57.     * @param key
  58.     * @return
  59.     */
  60.    public String get(String key) {
  61.        String str = stringRedisTemplate.opsForValue().get(key);
  62.        return str;
  63.   }
  64.    /**
  65.     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
  66.     * @param key key值
  67.     * @param id id值
  68.     * @param tClass 类型
  69.     * @param function 手写方法
  70.     * @param timeTTL 过期时间
  71.     * @param timeUnit 时间单位
  72.     * @param <T> 对象类型
  73.     * @param <ID> id类型
  74.     * @return 对象
  75.     */
  76.    public <T,ID> T getWithPassThrough(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
  77.        //1.判断redis中是否存在该id的数据
  78.        String str = get(key);
  79.        if (StrUtil.isNotBlank(str)) {
  80.            //2.存在 直接返回数据
  81.            return JSONUtil.toBean(str,tClass);
  82.       }
  83.        //上面判断后 执行到这句的时候,只能是null或者空字符串
  84.        if (str != null) {
  85.            return null;
  86.       }
  87.        //3.不存在 查询数据库是否存在
  88.        T shop = function.apply(id);
  89.        if (StringUtils.isEmpty(shop)) {
  90.            //将null存入到redis中
  91.            set(key,"",CACHE_NULL_TTL,timeUnit);
  92.            //4.不存在直接返回 404
  93.            return null;
  94.       }
  95.        //5.存在将数据存储在redis,然后返回
  96.        set(key,shop,timeTTL,timeUnit);
  97.        return shop;
  98.   }
  99.    //弄一个线程池
  100.    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  101.    /**
  102.     * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
  103.     * @param key key
  104.     * @param id id
  105.     * @param tClass RedisDate中存储对象类型
  106.     * @param function 方法
  107.     * @param timeTTL 过期时间
  108.     * @param timeUnit 过期类型
  109.     * @param <T> 对象类型
  110.     * @param <ID> id类型
  111.     * @return 对象
  112.     */
  113.    public <T,ID> T getWithLogicalExpire(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
  114.        //1.判断redis中是否存在该id的数据
  115.        String str = get(key);
  116.        if (StrUtil.isBlank(str)) {
  117.            //2.不存在 直接返回空
  118.            return null;
  119.       }
  120.        //3.存在 判断缓存是否过期 逻辑时间
  121.        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
  122.        LocalDateTime expireTime = redisData.getExpireTime();
  123.        JSONObject data = (JSONObject)redisData.getData();
  124.        T shop = JSONUtil.toBean(data, tClass);
  125.        if (expireTime.isAfter(LocalDateTime.now())) {
  126.            //4.未过期 直接返回商铺信息
  127.            return shop;
  128.       }
  129.        //5.过期了尝试获取互斥锁
  130.        String lockKey = LOCK_SHOP_KEY + id;
  131.        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
  132.        boolean flag = BooleanUtil.isTrue(aBoolean);
  133.        if (!flag) {
  134.            //6.如果未获取到锁 直接返回旧数据
  135.            return shop;
  136.       }
  137.        //7.成功获取到锁 开启一个独立线程
  138.        CACHE_REBUILD_EXECUTOR.submit(() -> {
  139.            //重构缓存
  140.            try {
  141.                //查询店铺数据
  142.                T tshop = function.apply(id);
  143.                //模拟
  144.                Thread.sleep(200);
  145.                //封装逻辑过期时间
  146.                setWithLogicalExpire(key,tshop,timeTTL,timeUnit);
  147.           } catch (InterruptedException e) {
  148.                e.printStackTrace();
  149.           }finally {
  150.                stringRedisTemplate.delete(lockKey);
  151.           }
  152.            //释放锁
  153.       });
  154.        //8.返回旧的数据
  155.        return shop;
  156.   }
  157. }

测试:ShopServiceImpl

  1.  @Override
  2.    public Result selectShopInfoById(Long id) throws InterruptedException {
  3.        //1.缓存穿透 存储null值解决方案
  4. //     return cacheShopWithPassThrough(id);
  5.        //2.缓存击穿 --互斥锁解决方案
  6. //       return cacheShopWithMutex(id);
  7.        //3.缓存击穿 ---逻辑过期解决方案
  8. //       return cacheShopWithLogicTTL(id);
  9.        //4.使用封装类中解决缓存穿透 存储null值办法
  10. //       Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY + id, id, Shop.class
  11. //                           ,this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
  12. //       return Result.ok(shop);
  13.        //5.使用封装类中解决缓存击穿 逻辑过期方式
  14.        //为了测试 将逻辑过期时间设置短一点
  15.        Shop shop = cacheClient.getWithLogicalExpire(CACHE_SHOP_KEY + id, id, Shop.class
  16.               , this::getById, 10L, TimeUnit.SECONDS);
  17.        return Result.ok(shop);
  18.   }

具体代码

redis实战篇-hmdp-短信登录-商铺缓存: 存放黑马点评中redis进行短信登录、商铺查询的代码 ,包括前端后后端

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/682899
推荐阅读
相关标签
  

闽ICP备14008679号