当前位置: 首页>数据库>正文

商品模块rediss缓存 商城的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 ),是存储数据的临时地方,一般读写性能较高

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据,第1张

 

缓存的作用:

  • 降低后端负载 ---直接访问缓存,返回数据
  • 提高读写效率,降低响应时间---基于内存存储

缓存的成本:

  • 数据一致性成本
  • 代码维护成本----解决一致性问题代码复杂
  • 运维成本-- 要保证高可用搭建集群

5.2 添加Redis缓存

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_商品模块rediss缓存_02,第2张

 

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

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_03,第3张

 

3、业务流程图:

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_04,第4张

 

4、代码实现

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

5、练习题

给店铺类型业务添加缓存

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_05,第5张

 

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

        return Result.ok(shopTypes);
    }

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. 删除缓存还是更新缓存?
  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失败,查询时再更新缓存(比较符合)
  1. 如何保障缓存与数据库的操作同时成功或失败?
  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC等分布式事务方案
  1. 先操作缓存还是先操作数据库?
  • 先删除缓存,再操作数据库(不推荐,更新数据库时间长,出现概率很大)第一个线程删除缓存后,在更新数据库的时候,还没更新成功的时候, 第二个线程访问了,发现缓存没有,查询数据库的数据,这是数据库的数据的旧的,将旧的数据更新到缓存中出现了不一致性

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

  • 先操作数据库,再删除缓存(推荐,相较于上一种出现概率很低)因为某种原因,缓存找中数据没了,线程1访问的时候发现没有缓存,查询数据库得到旧数据,要进行写入缓存操作时 线程2进行了更新数据库,删除缓存,然后线程1更新了缓存为旧数据

3、总结

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

  1. 低一致性需求:使用Redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
  • 读操作 Cache Aside Pattern(旁路缓存模式):
  • 缓存未命则直接返回
  • 缓存未命中则查询数据库,并写入缓存,设定超时时间
  • 写操作:
  • 先写数据库,然后再删除缓存
  • 要确保数据库与缓存操作的原子性

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

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

  1. 根据id查询商铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
@Override
    public Result selectShopInfoById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return Result.ok(JSONUtil.toBean(str,Shop.class));
        }
        //3.不存在 查询数据库是否存在
        Shop shop = baseMapper.selectById(id);
        if (StringUtils.isEmpty(shop)) {
            //4.不存在直接返回 404
            return Result.fail("店铺不存在");
        }
        //5.存在将数据存储在redis,然后返回
        String shopJsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }
  1. 根据id修改店铺时,先修改数据库,再删除缓存
@Override
    @Transactional
    public Result updateShopById(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        //修改数据库
        baseMapper.updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
        return Result.ok();
    }

5.4 缓存穿透

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

1、解决方案

常见的解决方案有两种:

  • 缓存空对象
  • 优点:实现简单,维护方便
  • 缺点:
  • 额外的内存消耗 ---设置ttl过期时间
  • 可能造成短期的不一致 ----插入数据的时候,更新缓存将null的覆盖
  • 商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_商品模块rediss缓存_06,第6张

  •  
  • 布隆过滤
  • 优点:内存占用较少,没有多余key
  • 缺点:
  • 实现复杂
  • 存在误判可能---布隆过滤器是居于hash算法,存在哈希碰撞问题
    判断不存在的肯定不存在,判断存在的时候,可能不存在

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_07,第7张

 

public Result selectShopInfoById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return Result.ok(JSONUtil.toBean(str,Shop.class));
        }
        //上面判断后 执行到这句的时候,只能是null或者空字符串
        if (str != null) {
            return Result.fail("店铺不存在");
        }

        //3.不存在 查询数据库是否存在
        Shop shop = baseMapper.selectById(id);
        if (StringUtils.isEmpty(shop)) {
            //将null存入到redis中
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            //4.不存在直接返回 404
             return Result.fail("店铺不存在");
        }
        //5.存在将数据存储在redis,然后返回
        String shopJsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }



3、总结

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

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

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

  • 缓存null值
  • 布隆过滤器
  • 增强id的复杂度,避免被猜测id规律,然后做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

5.5 缓存雪崩

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

解决方案:

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

5.6 缓存击穿

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_08,第8张

 

1、解决方案

互斥锁

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据库_09,第9张

 

逻辑过期

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据库_10,第10张

 

比较

解决方案

优点

缺点

互斥锁

没有额外的内存消耗 保证了一致性 实现简单

线程需要等待,性能受影响 可能有死锁的情况

逻辑过期

线程无序等待,性能好

不保证一致性 存在内存消耗 实现复杂

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

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_11,第11张

 

private  Result cacheShopWithMutex(Long id) {
            String key = CACHE_SHOP_KEY + id;
            Shop shop = null;
            //1.判断redis中是否存在该id的数据
            String str = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(str)) {
                //2.存在 直接返回数据
                return Result.ok(JSONUtil.toBean(str,Shop.class));
            }
            //上面判断后 执行到这句的时候,只能是null或者空字符串
            if (str != null) {
                return Result.fail("店铺不存在");
            }
        String lockKey = LOCK_SHOP_KEY + id;
        try {

            //3.不存在 先尝试获取互斥锁  利用redis中string字符串中set
            Boolean flagBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
            boolean flag = BooleanUtil.isTrue(flagBoolean);
            //4.获取锁失败
            if (!flag) {
                //获取锁失败休眠一会
                Thread.sleep(100);
                //然后进行重试 ---递归
                return selectShopInfoById(id);
            }
            //5.如果获取锁成功 查询数据库
            shop = baseMapper.selectById(id);
            //模拟重建延迟
            Thread.sleep(200);
            //如果数据库中没有数据
            if (StringUtils.isEmpty(shop)) {
                //将null存入到redis中
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //不存在直接返回 404
                return Result.fail("店铺不存在");
            }
            //.存在将数据存储在redis,然后返回
            String shopJsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //6.释放锁
            stringRedisTemplate.delete(lockKey);
        }
        return Result.ok(shop);
    }

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

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

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_12,第12张

 

//弄一个线程池
    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    //逻辑过期
    private Result cacheShopWithLogicTTL(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(str)) {
            //2.不存在 直接返回空
            return Result.fail("商铺信息为空");
        }


        //3.存在 判断缓存是否过期  逻辑时间
        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        if (expireTime.isAfter(LocalDateTime.now())) {
            //4.未过期 直接返回商铺信息
            return Result.ok(shop);
        }
        //5.过期了尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        boolean flag = BooleanUtil.isTrue(aBoolean);
        if (!flag) {
            //6.如果未获取到锁 直接返回旧数据
            return Result.ok(shop);
        }
        //7.成功获取到锁 开启一个独立线程
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            //重构缓存
            try {
                saveShopToRedis(id,30L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                stringRedisTemplate.delete(lockKey);
            }
            //释放锁
        });
        //8.返回旧的数据
        return Result.ok(shop);
    }

4、JMeter下载和安装

1.下载

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据库_13,第13张

2.解压

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_14,第14张

 

 

3.设置环境变量

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_商品模块rediss缓存_15,第15张

 

4.path中设置

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_16,第16张

 

5.启动

双击打开bin中的jemter.bat

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_17,第17张

 

就自动启动了

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_商品模块rediss缓存_18,第18张

 

6.设置中文

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据_19,第19张

 

7.进行配置

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_20,第20张

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_21,第21张

 

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_缓存_22,第22张

 

 

8、输入参数,测试

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_数据_23,第23张

 

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_商品模块rediss缓存_24,第24张

商品模块rediss缓存 商城的redis怎么做商品缓存,商品模块rediss缓存 商城的redis怎么做商品缓存_redis_25,第25张

 

 

5.7 缓存工具封装

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

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

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

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

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

封装类:CacheClient

package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * @packageName: com.hmdp.utils
 * @author: winter
 * @date: 2023/4/25 8:55
 * @version: 1.0
 * @email 1660420659@qq.com
 * @description: 封装Redis工具类
 */
@Slf4j
@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 将任意Java对象序列化为json并存储在String类型的key中,
     * 并且可设置TTL过期时间
     * @param key  key
     * @param obj  存储对象
     * @param timeTTL  过期时间
     * @param timeUnit  单位
     */
    public void set(String key, Object obj, Long timeTTL, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj),timeTTL,timeUnit);
    }

    /**
     * 将任意Java对象序列化为json并存储在String类型的key中,
     * 并在可以设计逻辑过期时间,用于处理缓存击穿问题
     * @param key  key
     * @param obj  存储对象
     * @param timeTTL  过期时间
     * @param timeUnit  单位
     */
    public  void setWithLogicalExpire(String key,Object obj,Long timeTTL, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(obj);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeTTL)));
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    /**
     * 通过key获取字符串
     * @param key
     * @return
     */
    public String get(String key) {
        String str = stringRedisTemplate.opsForValue().get(key);
        return str;
    }

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
     * @param key  key值
     * @param id  id值
     * @param tClass  类型
     * @param function 手写方法
     * @param timeTTL  过期时间
     * @param timeUnit  时间单位
     * @param <T>  对象类型
     * @param <ID> id类型
     * @return  对象
     */
    public <T,ID> T getWithPassThrough(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
        //1.判断redis中是否存在该id的数据
        String str = get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return JSONUtil.toBean(str,tClass);
        }
        //上面判断后 执行到这句的时候,只能是null或者空字符串
        if (str != null) {
            return null;
        }

        //3.不存在 查询数据库是否存在
        T shop = function.apply(id);
        if (StringUtils.isEmpty(shop)) {
            //将null存入到redis中
            set(key,"",CACHE_NULL_TTL,timeUnit);
            //4.不存在直接返回 404
            return null;
        }
        //5.存在将数据存储在redis,然后返回
        set(key,shop,timeTTL,timeUnit);
        return shop;
    }


    //弄一个线程池
    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
     * @param key key
     * @param id id
     * @param tClass  RedisDate中存储对象类型
     * @param function 方法
     * @param timeTTL 过期时间
     * @param timeUnit 过期类型
     * @param <T>  对象类型
     * @param <ID>  id类型
     * @return 对象
     */
    public <T,ID> T getWithLogicalExpire(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
        //1.判断redis中是否存在该id的数据
        String str = get(key);
        if (StrUtil.isBlank(str)) {
            //2.不存在 直接返回空
            return null;
        }


        //3.存在 判断缓存是否过期  逻辑时间
        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject)redisData.getData();
        T shop = JSONUtil.toBean(data, tClass);
        if (expireTime.isAfter(LocalDateTime.now())) {
            //4.未过期 直接返回商铺信息
            return shop;
        }
        //5.过期了尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        boolean flag = BooleanUtil.isTrue(aBoolean);
        if (!flag) {
            //6.如果未获取到锁 直接返回旧数据
            return shop;
        }
        //7.成功获取到锁 开启一个独立线程
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            //重构缓存
            try {
                //查询店铺数据
                T tshop = function.apply(id);
                //模拟
                Thread.sleep(200);
                //封装逻辑过期时间
                setWithLogicalExpire(key,tshop,timeTTL,timeUnit);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                stringRedisTemplate.delete(lockKey);
            }
            //释放锁
        });
        //8.返回旧的数据
        return shop;
    }
}

测试:ShopServiceImpl

@Override
    public Result selectShopInfoById(Long id) throws InterruptedException {
        //1.缓存穿透 存储null值解决方案
//     return  cacheShopWithPassThrough(id);
        //2.缓存击穿  --互斥锁解决方案
//       return cacheShopWithMutex(id);
        //3.缓存击穿 ---逻辑过期解决方案
//        return cacheShopWithLogicTTL(id);

        //4.使用封装类中解决缓存穿透  存储null值办法
//        Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY + id, id, Shop.class
//                            ,this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        return Result.ok(shop);

        //5.使用封装类中解决缓存击穿  逻辑过期方式
        //为了测试 将逻辑过期时间设置短一点
        Shop shop = cacheClient.getWithLogicalExpire(CACHE_SHOP_KEY + id, id, Shop.class
                , this::getById, 10L, TimeUnit.SECONDS);
        return Result.ok(shop);
    }


https://www.xamrdz.com/database/6k71928754.html

相关文章: