当前位置: 首页 > news >正文

2022黑马Redis跟学笔记.实战篇(三)

2022黑马Redis跟学笔记.实战篇 三

    • 4.2.商家查询的缓存功能
      • 4.3.1.认识缓存
        • 4.3.1.1.什么是缓存
        • 4.3.1.2.缓存的作用
          • 1.为什么要使用缓存
          • 2.如何使用缓存
          • 3. 添加商户缓存
          • 4. 缓存模型和思路
        • 4.3.1.3.缓存的成本
      • 4.3.2.添加redis缓存
      • 4.3.3.缓存更新策略
        • 4.3.3.1.三种策略
          • (1).内存淘汰:Redis自带的内存淘汰机制
          • (2).过期淘汰:利用expire命令给数据设置过期时间
          • (3).主动更新:主动完成数据库与缓存的同时更新
        • 4.3.3.2.策略选择
          • 4.3.3.2.1.低一致性需求
          • 4.3.3.3.2.高一致性需求
        • 4.3.3.3.主动更新的方案
          • 4.3.3.3.1.Cache Aside
            • 1.缓存调用者在更新数据库的同时完成对完成的更新
          • 4.3.3.3.2.Read/Write Through
          • 4.3.3.3.3.Write Back
        • 4.3.3.4.Cache Aside的模式选择
          • 4.3.3.4.1.更新缓存还是删除缓存?
          • 4.3.3.4.2.先操作数据库还是缓存?
          • 4.3.3.4.3.如何确保数据库与缓存操作原子性?
            • 4.3.3.4.3.1.单体系统
            • 4.3.3.4.3.2.分布式系统
        • 4.3.3.5.最佳实践
          • 4.3.3.5.1. 实现商铺和缓存与数据库双写一致
          • 4.3.3.5.2. 查询数据时
            • 1.先查询缓存
            • 2.如果缓存命中,直接返回
            • 3.如果缓存未命中,则查询数据库
            • 4.将数据库数据写入缓存
            • 5.返回结果
          • 4.3.3.5.3. 修改数据库时
            • 1.先修改数据库
            • 2.然后删除缓存
            • 3.确保两者的原子性
      • 4.3.4.缓存穿透
        • 4.3.4.1.产生原因(定义)
        • 4.3.4.2.缓存穿透问题的解决方案
          • 4.3.4.2.1.⭐方案一:缓存空对象
          • 4.3.4.2.2.方案二:布隆过滤
          • 4.3.4.2.2.其它
      • 4.3.5.缓存雪崩
        • 4.3.5.1 产生原因(定义)
        • 4.3.5.2 缓存穿透的解决方案
      • 4.3.6.缓存击穿(热点Key)
        • 4.3.6.1.产生原因
          • 4.3.6.1.1.热点Key
            • (1).在某一时段被高并发访问
            • (2).缓存重建耗时较长
        • 4.3.6.2.解决方案
          • 4.3.6.2.1.方案一:互斥锁
            • 1. 思路
            • 2. 优点
            • 3. 缺点
            • 4.利用互斥锁解决缓存击穿问题
          • 4.3.6.2.2.方案二:逻辑过期
            • 1.思路
            • 2.优点
            • 3.缺点
            • 4.利用逻辑过期解决缓存击穿问题
          • 4.3.6.2.3.方案对比
      • 4.3.7.Redis缓存工具封装

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2.商家查询的缓存功能

4.3.1.认识缓存

4.3.1.1.什么是缓存

就像自行车,越野车的避震器
在这里插入图片描述

举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;

同样,实际开发中,系统也需要"避震器",防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;

这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术;

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码(例如:

1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存例3:Static final Map<K,V> map =  new HashMap(); 本地缓存

由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;

缓存定义:

  • 一种具备高效读写能力的数据暂存区域。

4.3.1.2.缓存的作用

1.为什么要使用缓存

一句话:因为速度快,好用

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力。

实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术;
但是缓存也会增加代码复杂度和运营的成本:
在这里插入图片描述
总结来说,缓存的作用:

  • 1.降低后端负载
  • 2.提高服务读写响应速度
2.如何使用缓存

实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用。

浏览器缓存:主要是存在于浏览器端的缓存。

**应用层缓存:**可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存。

**数据库缓存:**在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中。

**CPU缓存:**当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存。
在这里插入图片描述

3. 添加商户缓存

在我们查询商户信息时,我们是直接操作从数据库中去进行查询的,大致逻辑是这样,直接查询数据库那肯定慢,所以我们需要增加缓存。

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {//这里是直接查询数据库return shopService.queryById(id);
}
4. 缓存模型和思路

标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis。
在这里插入图片描述
代码如下:
代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入redis。
修改ShopController.java

    @GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {// select * from tb_shop where id = ?return shopService.queryById(id);}

修改接口IShopService.java

public interface IShopService extends IService<Shop> {/*** @param* @return com.hmdp.dto.Result* @description //通过id定位商户* @param: id* @date 2023/2/12 22:43* @author wty**/Result queryById(Long id);
}

修改ShopMapper.java,增加注解

@Mapper
public interface ShopMapper extends BaseMapper<Shop> {}

修改ShopServiceImpl.java

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Autowiredprivate ShopMapper shopMapper;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {// 1.从Redis中查询商铺缓存 cache:shop: + idString shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);// 2.判断缓存是否命中if (StrUtil.isNotBlank(shopJson)) {// 2.1如果存在直接返回商铺信息Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);} else {// 2.2如果不存在// 2.2.1根据id查询数据库的商铺信息LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Shop::getId, id);Shop shop = shopMapper.selectOne(wrapper);// 2.2.2 如果不存在,直接返回错误if (null == shop) {return Result.fail("您要查询的店铺不存在!");} else {// 2.2.2 如果存在,先写入Redis然后返回String jsonShop = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, jsonShop);return Result.ok(shop);}}

修改RedisConstants.java

   public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";

重启项目
点击101茶餐厅
在这里插入图片描述
查看Redis
在这里插入图片描述
再刷新一下界面,发现缓存生效
在这里插入图片描述
给店铺的类型做一个缓存
在这里插入图片描述
修改ShopTypeController.java

@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {@Resourceprivate IShopTypeService typeService;@GetMapping("list")public Result queryTypeList() {return typeService.queryTypeList();}
}

修改IShopTypeService.java

public interface IShopTypeService extends IService<ShopType> {/*** @param* @return com.hmdp.dto.Result* @description //查询店铺类别* @date 2023/2/13 0:11* @author wty**/Result queryTypeList();
}

修改ShopTypeServiceImpl.java

@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryTypeList() {// 1.从Redis中查询商铺种类Long size = stringRedisTemplate.opsForList().size(RedisConstants.CACHE_SHOP_TYPE_KEY);//特殊用法:通过stringRedisTemplate.opsForList().range(key, 0, -1)可以查询索引第一个到索引倒数第一个(即所有数据)List<String> lists = stringRedisTemplate.opsForList().range(RedisConstants.CACHE_SHOP_TYPE_KEY, 0, size - 1);// 2.查询商铺种类是否存在if (!lists.isEmpty()) {// 2.1商铺种类对象如果存在,则返回该对象ArrayList<ShopType> typeList = new ArrayList<>();for (String list : lists) {typeList.add(JSONUtil.toBean(list, ShopType.class));}return Result.ok(typeList);} else {// 2.2商铺种类对象如果不存在,则查询数据库List<ShopType> typeList = query().orderByAsc("sort").list();if (typeList.isEmpty()) {return Result.fail("店铺分类不存在!");} else {// 3.如果存在就保存到redis中for (ShopType shopType : typeList) {stringRedisTemplate.opsForList().rightPushAll(RedisConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopType));}// 4.返回店铺种类return Result.ok(typeList);}}}
}

重启应用,发现启动时间如下
在这里插入图片描述
查看redis中数据如下
在这里插入图片描述
再刷新界面,时间明显减少
在这里插入图片描述

4.3.1.3.缓存的成本

  • 1.开发成本
  • 2.运维成本
  • 3.一致性问题

4.3.2.添加redis缓存

4.3.3.缓存更新策略

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。

4.3.3.1.三种策略

(1).内存淘汰:Redis自带的内存淘汰机制

redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)。

(2).过期淘汰:利用expire命令给数据设置过期时间

当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存。

(3).主动更新:主动完成数据库与缓存的同时更新

我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题。
在这里插入图片描述

4.3.3.2.策略选择

4.3.3.2.1.低一致性需求

使用内存淘汰机制,例如店铺类型的查询缓存。

4.3.3.3.2.高一致性需求

主动更新为主,超时剔除作为兜底方案。例如店铺详情查询的缓存。

4.3.3.3.主动更新的方案

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:

用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢?主动更新有如下几种方案:

  • 方案一:Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。

  • 方案二:Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理。

  • 方案三:Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致。
    在这里插入图片描述

4.3.3.3.1.Cache Aside
1.缓存调用者在更新数据库的同时完成对完成的更新
  • 一致性良好
  • 实现难度一般
4.3.3.3.2.Read/Write Through

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

  • 一致性优秀
  • 实现复杂
  • 性能一般
4.3.3.3.3.Write Back

缓存调用者的CRUD都针对缓存完成。由独立线程异步的将缓存数据写到数据库,实现最终一致。

  • 一致性差
  • 性能好
  • 实现复杂

4.3.3.4.Cache Aside的模式选择

综合考虑使用方案一Cache Aside,但是方案一调用者如何处理呢?这里有几个问题:
操作缓存和数据库时有三个问题需要考虑:

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来

4.3.3.4.1.更新缓存还是删除缓存?
  • 更新缓存会产生无效更新,并且存在较大的线程安全问题,每次更新数据库都更新缓存,无效写操作较多。

  • 删除缓存本质是延迟更新,没有无效更新,线程安全问题相对较低,更新数据库时让缓存失效,查询时再更新缓存。

4.3.3.4.2.先操作数据库还是缓存?
  • √(1).先更新数据,再删除缓存

  • 在满足原子性的情况下,安全问题概率较低

  • (2).先删除缓存,再更新数据库

  • 安全问题概率较高

4.3.3.4.3.如何确保数据库与缓存操作原子性?
4.3.3.4.3.1.单体系统

将缓存与数据库操作放在一个事务。

4.3.3.4.3.2.分布式系统
  • 利用分布式事务(TCC)机制。

4.3.3.5.最佳实践

应该具体操作缓存还是操作数据库,我们应当是先操作数据库,再删除缓存,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

  • 先操作缓存还是先操作数据库?
    • 先删除缓存,再操作数据库
    • 先操作数据库,再删除缓存

在这里插入图片描述
总结
在这里插入图片描述

4.3.3.5.1. 实现商铺和缓存与数据库双写一致

核心思路如下:

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

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

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

修改重点代码1:修改ShopServiceImpl的queryById方法

设置redis缓存时添加过期时间
ShopServiceImpl.java

                // 3.设置缓存的超时时间stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

在这里插入图片描述

修改重点代码2

代码分析:通过之前的淘汰,我们确定了采用删除策略,来解决双写问题,当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从mysql中加载最新的数据,从而避免数据库和缓存不一致的问题

修改ShopController.java

    @PutMappingpublic Result updateShop(@RequestBody Shop shop) {// 写入数据库return shopService.updateShop(shop);}

更新

Result updateShop(Shop shop);

更新ShopServiceImpl.java

@Override@Transactionalpublic Result updateShop(Shop shop) {// 1.更新mysql数据库updateById(shop);// 2.因为下面是根据id进行删除,所以必须要保证id存在if (null != shop.getId()) {// 3.删除redis缓存stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());}// 4.返回return Result.ok();}

启动程序
在这里插入图片描述
看一下终端后台
在这里插入图片描述
查询一下redis数据库
在这里插入图片描述
刷新一下,看一下TTL,设置了有效期
在这里插入图片描述

借助postMan工具进行更新操作(因为更新是管理员操作,这里做一下模拟)
更改名字为新疆绝味烤肉
在这里插入图片描述点击send后发现是405
在这里插入图片描述
原来是地址错误了,我们要用nginx的地址
更换地址
在这里插入图片描述
发现也是报错,原来是更新的时候,时间戳不能还是原来那个
在这里插入图片描述
于是在PostMan的json中去掉坐标和时间的属性
在这里插入图片描述
再点击send,说明更新成功了
在这里插入图片描述
我们看IDEA,也有更新的语句
在这里插入图片描述
数据库中更改了
在这里插入图片描述
再看Redis中没有数据了
在这里插入图片描述

再次刷浏览器,发现名字被修改了。
在这里插入图片描述

4.3.3.5.2. 查询数据时
1.先查询缓存
2.如果缓存命中,直接返回
3.如果缓存未命中,则查询数据库
4.将数据库数据写入缓存
5.返回结果
4.3.3.5.3. 修改数据库时
1.先修改数据库
2.然后删除缓存
3.确保两者的原子性

4.3.4.缓存穿透

4.3.4.1.产生原因(定义)

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

4.3.4.2.缓存穿透问题的解决方案

4.3.4.2.1.⭐方案一:缓存空对象
  • (1).思路
    对于不存在的数据也在Redis建立缓存,值为空,并设置一个较短的TTL时间。
    当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了。
  • (2).优点
    实现简单,维护方便。
  • (3).缺点
    额外的内存消耗。
    短期的数据不一致问题。

在这里插入图片描述

4.3.4.2.2.方案二:布隆过滤
  • (1).思路
    利用布隆过滤算法,在请求进入Redis之前先判断是否存在,如果不存在则直接拒绝请求。
    布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
    假设布隆过滤器判断这个数据不存在,则直接返回
    这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。
    在这里插入图片描述
  • (2).优点
    内存占用少。
  • (3).缺点
    实现复杂
    存在误判的可能性
4.3.4.2.2.其它
  • (1).做好数据的基础格式校验
  • (2).加强用户权限校验
  • (3).做好热点参数的限流

核心思路如下:

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的

现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
在这里插入图片描述
这里讲一下查看源码帮助文档的方式
打开Terminal,与当前项目中pom.xml的统计目录输入以下指令

mvn dependency:sources
mvn dependency:resolve -Dclassifier=javadoc

如果只想单独下载某个依赖, 可以输入

mvn dependency:sources -DincludeArtifactIds=spring-rabbit

这时候鼠标悬浮到想看的代码,就会有注释
在这里插入图片描述

更改ShopServiceImpl.java
在这里插入图片描述
在这里插入图片描述

代码如下:ShopServiceImpl.java

    @Overridepublic Result queryById(Long id) {// 1.从Redis中查询商铺缓存 cache:shop: + idString shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);// 2.判断缓存是否命中if (StrUtil.isNotBlank(shopJson)) {// 2.1如果存在直接返回商铺信息Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);} else {//现在shopJson有2种可能,空字符串和nullif ("".equals(shopJson)) {// 如果是空字符串,返回一个错误信息return Result.fail("您要查询的店铺不存在!");}// 2.2如果为null,就进行下面的操作// 2.2.1根据id查询数据库的商铺信息LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Shop::getId, id);Shop shop = shopMapper.selectOne(wrapper);// 2.2.2 如果不存在,直接返回错误if (null == shop) {// 将空值写入redis并且设置有效期(缓存穿透)stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("您要查询的店铺不存在!");} else {// 2.2.2 如果存在,先写入Redis然后返回String jsonShop = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, jsonShop);// 3.设置缓存的超时时间stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 返回return Result.ok(shop);}}}

修改RedisConstants.java

public static final Long CACHE_NULL_TTL = 2L;

重启应用
在这里插入图片描述
打开连接如下:
在这里插入图片描述
发现IDEA后台输出查询语句
在这里插入图片描述
清空后刷新页面,输入一个没有的id
在这里插入图片描述
看IDEA后台有sql输出
在这里插入图片描述
清空IDEA后台后再刷新当前失败的跳转页,发现没有sql输出了,说明是用的Redis缓存,我们看一下Redis的图形界面
在这里插入图片描述
看以下TTL
在这里插入图片描述
同理,我们给店铺种类,也可以加上相同的缓存null的逻辑,防止缓存击穿
修改ShopTypeServiceImpl.java
在这里插入图片描述
在这里插入图片描述

ShopTypeServiceImpl.java

@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryTypeList() {// 1.从Redis中查询商铺种类Long size = stringRedisTemplate.opsForList().size(RedisConstants.CACHE_SHOP_TYPE_KEY);//特殊用法:通过stringRedisTemplate.opsForList().range(key, 0, -1)可以查询索引第一个到索引倒数第一个(即所有数据)List<String> lists = stringRedisTemplate.opsForList().range(RedisConstants.CACHE_SHOP_TYPE_KEY, 0, size - 1);// 2.查询商铺种类是否存在if (!lists.isEmpty()) {// 2.1商铺种类对象如果存在,则返回该对象ArrayList<ShopType> typeList = new ArrayList<>();for (String list : lists) {typeList.add(JSONUtil.toBean(list, ShopType.class));}return Result.ok(typeList);} else {// 现在lists有2种可能,空字符串和nullif ("".equals(lists)) {// 如果是空字符串,返回一个错误信息return Result.fail("您要查询的店铺种类不存在!");}// 2.2商铺种类对象如果不存在,则查询数据库List<ShopType> typeList = query().orderByAsc("sort").list();if (typeList.isEmpty()) {// 设置空值防止缓存击穿stringRedisTemplate.opsForList().rightPush(RedisConstants.CACHE_SHOP_TYPE_KEY, "");return Result.fail("店铺分类不存在!");} else {// 3.如果存在就保存到redis中for (ShopType shopType : typeList) {stringRedisTemplate.opsForList().rightPush(RedisConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopType));// 设置缓存时间stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_TYPE_KEY, RedisConstants.CACHE_SHOP_TYPE__TTL, TimeUnit.MINUTES);}// 4.返回店铺种类return Result.ok(typeList);}}}

总结:

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

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

缓存穿透的解决方案有哪些?
被动措施:

  • 缓存null值
  • 布隆过滤

主动措施:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

4.3.5.缓存雪崩

4.3.5.1 产生原因(定义)

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

4.3.5.2 缓存穿透的解决方案

  • (1).给不同的Key的TTL添加随机值
  • (2).利用Redis集群提高服务的可用性
  • (3).给缓存业务添加降级限流策略
  • (4).给业务添加多级缓存
    在这里插入图片描述

4.3.6.缓存击穿(热点Key)

4.3.6.1.产生原因

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

4.3.6.1.1.热点Key

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大。
在这里插入图片描述

(1).在某一时段被高并发访问
(2).缓存重建耗时较长

常见的缓存击穿问题解决方案有两种。

4.3.6.2.解决方案

4.3.6.2.1.方案一:互斥锁
1. 思路

给缓存重建过程加锁,确保重建过程只有一个线程执行,其它线程等待。
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
在这里插入图片描述

2. 优点

实现简单
没有额外内存消耗
一致性好

3. 缺点

等待导致性能下降
有死锁风险

4.利用互斥锁解决缓存击穿问题

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿。

在这里插入图片描述
操作锁的代码:

核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1;如果当前key被setnx赋值,其它指令再想赋值的时候会返回0,可以当作一个互斥锁。
在这里插入图片描述
那这个互斥锁,如何释放呢,我们模拟以下,只需要删除该属性即可。
在这里插入图片描述
特别注意如果程序错误,没有删除掉该锁,那么后续业务会一直处于等待的状态,这样是不好的,那么我们的解决方案就是给锁加上有效期。

在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。

修改ShopServiceImpl.java
先增加获取锁的逻辑

    //尝试获取锁private boolean tryLock(String key) {// setnx = setIfAbsent// 这里注意获取锁等待时间是10秒Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);// 这里注意flag是包装类Boolean,而方法的返回值是基本类型boolean// 这里要拆箱后再返回结果return BooleanUtil.isTrue(flag);}//释放锁private void unLock(String key) {stringRedisTemplate.delete(key);}

修改RedisConstants.java添加

public static final Long LOCK_SHOP_TTL = 10L;

修改ShopServiceImpl.java

@Overridepublic Result queryById(Long id) {// 解决缓存穿透//Shop shop = queryWithPassThough(id);// 互斥锁解决缓存击穿Shop shop = queryWithMutex(id);if (null == shop) {Result.fail("您要查询的店铺不存在!");}return Result.ok(shop);}

缓存击穿核心处理逻辑ShopServiceImpl.java

public Shop queryWithMutex(Long id) {boolean flag;Shop shop = null;try {do {// 1.从Redis中查询商铺缓存 cache:shop: + idString shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);// 2.判断缓存是否命中if (StrUtil.isNotBlank(shopJson)) {// 2.1如果存在直接返回商铺信息shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//现在shopJson有2种可能,空字符串和nullif (null != shopJson) {// 如果是空字符串,返回一个错误信息//return Result.fail("您要查询的店铺不存在!");return null;}// 1.1解决缓存击穿:尝试获取锁flag = tryLock(RedisConstants.LOCK_SHOP_KEY);// 1.2判断互斥锁是否获取成功if (!flag) {// 1.2.1获取失败 就休眠一会儿,然后查询Redis 的shopThread.sleep(50);}} while (flag == false);// 1.2.2获取成功 就直接进行下面的步骤// 2.2如果为null,就进行下面的操作// 2.2.1根据id查询数据库的商铺信息LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Shop::getId, id);shop = shopMapper.selectOne(wrapper);// TODO 模拟Redis击穿后,重建Redis的时间,可以去掉Thread.sleep(200);// 2.2.2 如果不存在,直接返回错误if (null == shop) {// 将空值写入redis并且设置有效期(缓存穿透)stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);//return Result.fail("您要查询的店铺不存在!");return null;}// 2.2.2 如果存在,先写入Redis然后返回String jsonShop = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, jsonShop);// 3.设置缓存的超时时间stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 4.释放锁unLock(RedisConstants.LOCK_SHOP_KEY);}// 5.返回return shop;}

注意:do{}while(条件),是说条件不成立跳出循环,条件成立,继续循环
重启程序
打开压力测试工具JMeter
定义1000个线程
循环次数5秒
在这里插入图片描述
Http配置如下
在这里插入图片描述
点击发起请求
在这里插入图片描述
查看结果树
在这里插入图片描述
查看汇总报告
在这里插入图片描述

4.3.6.2.2.方案二:逻辑过期
1.思路
  • 热点key缓存永不过期,而是设置一个逻辑过期时间,查询到数据时通过对逻辑过期时间判断,来决定是否需要重建缓存。
  • 重建缓存也通过互斥锁保证单线程执行。
  • 重建缓存利用独立线程异步执行。
  • 其它线程无需等待,直接查询到的旧数据即可。

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

在这里插入图片描述

2.优点

线程无需等待,性能较好。

3.缺点

不保证一致性。
有额外内存消耗。
实现复杂。

4.利用逻辑过期解决缓存击穿问题

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

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
在这里插入图片描述
如果封装数据:因为现在redis中存储的数据的value需要带上过期时间,此时要么你去修改原来的实体类,要么你新建一个实体类,我们采用第二个方案,这个方案,对原来代码没有侵入性。

步骤一、

新增类RedisData .java添加逻辑过期时间
在这里插入图片描述
RedisData.java代码如下

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RedisData {private LocalDateTime expireTime;/*** data可以理解存储的是Shop对象*/private Object data;}

步骤二、

ShopServiceImpl 新增此方法,利用单元测试进行缓存预热
在这里插入图片描述
修改ShopServiceImpl.java
增加一个方法,用来保存和设置有效期

    public void saveShop2Redis(Long id, Long seconds) {// 1.查询shop数据LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Shop::getId, id);// select * from tb_shop where id = ?Shop shop = shopMapper.selectOne(wrapper);// 2.封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));// 3.写入RedisstringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

在单元测试类中测试
代码如下
CommentApplicationTests.java

@SpringBootTest
class CommentApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Testpublic void testSave() {shopService.saveShop2Redis(1L, 10L);}}

点击测试
在这里插入图片描述
查看后台
在这里插入图片描述
查看Redis图形界面,有效期存进去了
在这里插入图片描述

步骤三:正式代码

修改ShopServiceImpl,逻辑过期核心代码

public Shop queryWithLogicalExpire(Long id) {// 1.从Redis中查询商铺缓存 cache:shop: + idString redisData_Str = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);// 2.判断缓存是否命中if (StrUtil.isBlank(redisData_Str)) {// 2.1如果未命中Redis缓存信息,则直接返回空return null;}// 2.2如果命中了Redis缓存信息,判断缓存是否过期RedisData redisData = JSONUtil.toBean(redisData_Str, RedisData.class);Object o = redisData.getData();JSONObject data = (JSONObject) o;Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 3.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 3.1未过期,直接返回店铺信息return shop;}// 3.2已过期,需要缓存重建// 4 获取互斥锁boolean flag = tryLock(RedisConstants.LOCK_SHOP_KEY + id);// 5. 判断是否获取锁成功if (flag) {// 5.1 获取到锁 首先再判断是否过期,过期// 然后就开启线程 根据id查询数据库  将商铺数据写入Redis并设置逻辑过期时间,释放互斥锁ExecutorService executorService = newFixedThreadPool(10);/*executorService.submit(new Runnable() {@Overridepublic void run() {try {// 重建缓存// TODO saveShop2Redis(id, RedisConstants.CACHE_SHOP_TYPE__TTL);saveShop2Redis(id, 20L);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁unLock(RedisConstants.LOCK_SHOP_KEY + id);}}});*/// 函数式接口executorService.submit(() -> {try {//再次检查缓存有没有过期,防止在高并发环境下缓存多次重建LocalDateTime time = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id),RedisData.class).getExpireTime();// 3.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 3.1未过期,直接返回店铺信息// run方法是无参的,所以这里没有返回值return;}// 重建缓存// TODO saveShop2Redis(id, RedisConstants.CACHE_SHOP_TYPE__TTL);saveShop2Redis(id, 20L);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁unLock(RedisConstants.LOCK_SHOP_KEY + id);}});}// 5.2未获取到锁还是获取到锁都要 返回商铺信息同3.1return shop;}

修改ShopServiceImpl.java,添加线程池

    public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

修改RedisConstants.java
添加锁的key和有效期10秒

    public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;

为了测试一致性
先更改数据库信息
在这里插入图片描述

UPDATE tb_shop SET NAME = '102茶餐厅' WHERE id = 1;

此时Redis中数据为
在这里插入图片描述

重启应用
继续使用JMeter
配置如下
在这里插入图片描述
在这里插入图片描述

运行后发现,查询结果是旧的。
在这里插入图片描述
睡眠了50ms后更新成最新的了
在这里插入图片描述

查看Redis,已经是最新的了
在这里插入图片描述
后台只有一次sql的查询
在这里插入图片描述
验证了:不保证一致性

4.3.6.2.3.方案对比

**互斥锁方案:**由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响。

逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦。

在这里插入图片描述

4.3.7.Redis缓存工具封装

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

  • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间。
  • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓。

存击穿问题

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

将逻辑进行封装
新建工具类CacheClient.java
在这里插入图片描述
CacheClient.java代码如下:

@Component
@Slf4j
public class CacheClient {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** @param* @return void* @description //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间* @param: key* @param: value* @param: time* @param: unit* @date 2023/2/14 9:17* @author wty**/public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}/*** @param* @return void* @description //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题* @param: key* @param: value* @param: time* @param: unit* @date 2023/2/14 9:20* @author wty**/public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);// 把时间转换成秒redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, unit);}/*** @param* @return T 泛型(实体类的类型)* @description // 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题* @param: keyPrefix 设置一个常量* @param: id 泛型(可以为任意类型)* @param: type T的类型* @param: function* @param: time 有效期* @param: unit 有效期的时间单位:分钟/小时/秒* @date 2023/2/14 9:48* @author wty**/public <T, ID> T queryWithPassThough(String keyPrefix, ID id, Class<T> type, Function<ID, T> function, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从Redis中查询实体对象的缓存,例如商铺就是 cache:shop: + idString json = stringRedisTemplate.opsForValue().get(key);// 2.判断缓存是否命中if (StrUtil.isNotBlank(json)) {// 2.1如果存在直接返回对象信息T t = JSONUtil.toBean(json, type);return t;} else {//现在json有2种可能,空字符串和nullif ("".equals(json)) {// 如果是空字符串,返回一个空,外层调用的时候抛出错误信息//return Result.fail("您要查询的店铺不存在!");return null;}// 2.2如果为null,就进行下面的操作// 2.2.1根据id查询数据库的商铺信息,这里用函数式接口实现/* LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();// select * from tb_shop where id = ?wrapper.eq(Shop::getId, id);Shop shop = shopMapper.selectOne(wrapper);*/T t = function.apply(id);// 2.2.2 如果不存在,直接返回错误if (null == t) {// 将空值写入redis并且设置有效期(缓存穿透) 2分钟stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);// 上层返回以下信息//return Result.fail("您要查询的店铺不存在!");return null;} else {// 2.2.2 如果存在,先写入Redis然后返回,并且设置缓存的超时时间,使用上面的方法即可set(key, t, time, unit);// 返回return t;}}}
}

在ShopServiceImpl 中更改

@Resource
private CacheClient cacheClient;@Overridepublic Result queryById(Long id) {// 解决缓存穿透//Shop shop = queryWithPassThough(id);// 解决缓存穿透(使用模板)//cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, id_key -> getById(id_key), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 解决缓存穿透(使用模板) 方法引用Shop shop = cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);// 逻辑过期解决缓存击穿//Shop shop = queryWithLogicalExpire(id);if (null == shop) {Result.fail("您要查询的店铺不存在!");}return Result.ok(shop);}

重启应用,还用shop0作为测试用例
在这里插入图片描述
多次刷新,发现只有第一次用了sql查询数据库,剩下都是redis
在这里插入图片描述
发现空值已经存入redis
在这里插入图片描述
下面封装逻辑过期解决缓击击穿
修改CacheClient.java
添加代码

@Component
@Slf4j
public class CacheClient {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** @param* @return void* @description //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题* @param: key* @param: value* @param: time* @param: unit* @date 2023/2/14 9:20* @author wty**/public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);// 把时间转换成秒redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, unit);}/*** @param* @return T* @description //逻辑过期解决缓存击穿* @param: keyPrefix 常量 例如shop是 CACHE_SHOP_KEY = "cache:shop:"* @param: id 泛型 可以是任意类型* @param: type 泛型* @param: function 函数式接口,ID是参数 T是返回值类型* @param: time 有效期* @param: unit 有效期类型* @date 2023/2/14 12:14* @author wty**/public <T, ID> T queryWithLogicalExpire(String keyPrefix, ID id, Class<T> type, Function<ID, T> function, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从Redis中查询实体类缓存 例如shop是 cache:shop: + idString redisData_Str = stringRedisTemplate.opsForValue().get(key);// 2.判断缓存是否命中if (StrUtil.isBlank(redisData_Str)) {// 2.1如果未命中Redis缓存信息,则直接返回空return null;}// 2.2如果命中了Redis缓存信息,判断缓存是否过期RedisData redisData = JSONUtil.toBean(redisData_Str, RedisData.class);Object o = redisData.getData();JSONObject data = (JSONObject) o;T t = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();// 3.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 3.1未过期,直接返回实体类信息return t;}// 3.2已过期,需要缓存重建// 4 获取互斥锁boolean flag = tryLock(RedisConstants.LOCK_SHOP_KEY + id, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);// 5. 判断是否获取锁成功if (flag) {// 5.1 获取到锁 首先再判断是否过期,过期// 然后就开启线程 根据id查询数据库  将实体类数据写入Redis并设置逻辑过期时间,释放互斥锁ExecutorService executorService = newFixedThreadPool(10);/*executorService.submit(new Runnable() {@Overridepublic void run() {try {// 重建缓存// TODO saveShop2Redis(id, RedisConstants.CACHE_SHOP_TYPE__TTL);saveShop2Redis(id, 20L);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁unLock(RedisConstants.LOCK_SHOP_KEY + id);}}});*/// 函数式接口executorService.submit(() -> {try {//再次检查缓存有没有过期,防止在高并发环境下缓存多次重建LocalDateTime expireTime_Thread = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(key),RedisData.class).getExpireTime();// 3.判断是否过期if (expireTime_Thread.isAfter(LocalDateTime.now())) {// 3.1未过期,直接返回实体类信息// run方法是无参的,所以这里没有返回值return;}// 4.重建缓存// 4.1查询数据库T t_thread = function.apply(id);// 4.2写入RedissetWithLogicExpire(key, t_thread, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(RedisConstants.LOCK_SHOP_KEY + id);}});}// 5.2未获取到锁还是获取到锁都要 返回商铺信息同3.1return t;}/*** @param* @return java.util.concurrent.ExecutorService* @description //创建固定线程数的线程池* @param: nThreads* @date 2023/2/14 1:19* @author wty**/public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}/*** @param* @return boolean* @description //尝试获取锁* @param: key* @param: TTL 有效期,例如shop是RedisConstants.LOCK_SHOP_TTL* @param: unit 有效期的单位:比如小时/分钟/秒* @date 2023/2/13 16:37* @author wty**/private boolean tryLock(String key, Long TTL, TimeUnit unit) {// setnx = setIfAbsent// 这里注意获取锁等待时间是10秒Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", TTL, unit);// 这里注意flag是包装类Boolean,而方法的返回值是基本类型boolean// 这里要拆箱后再返回结果return BooleanUtil.isTrue(flag);}/*** @param* @return void* @description //释放锁* @param: key* @date 2023/2/13 16:45* @author wty**/private void unLock(String key) {stringRedisTemplate.delete(key);}}

修改ShopServiceImpl.java

    @Overridepublic Result queryById(Long id) {// 解决缓存穿透//Shop shop = queryWithPassThough(id);// 解决缓存穿透(使用模板)//cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, id_key -> getById(id_key), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 解决缓存穿透(使用模板) 方法引用//Shop shop = cacheClient//.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);// 逻辑过期解决缓存击穿Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.SECONDS);if (null == shop) {return Result.fail("您要查询的店铺不存在!");}return Result.ok(shop);}

修改CommentApplicationTests.java

@SpringBootTest
class CommentApplicationTests {@Resourceprivate CacheClient cacheClient;@Resourceprivate ShopServiceImpl shopService;@Testpublic void testLogicExpire() {Shop shop = shopService.getById(1L);cacheClient.setWithLogicExpire(RedisConstants.CACHE_SHOP_KEY, shop, 10L, TimeUnit.SECONDS);}
}

先跑测试类CommentApplicationTests,让Reds中有数据
在这里插入图片描述
点击运行测试类后,看到Redis中有数据
在这里插入图片描述
更新数据库

UPDATE tb_shop SET NAME = '104茶餐厅' WHERE id = 1;

然后运行JMeter,请求正常
在这里插入图片描述

之后我们修改一下利用互斥锁解决缓存击穿问题的代码变成工具类
修改CacheClient.java

public <T, ID> T queryWithMutex(String keyPrefix, ID id, Class<T> type, Function<ID, T> function, Long time, TimeUnit unit) {String key = keyPrefix + id;boolean flag;T t = null;try {do {// 1.从Redis中查询实体类的缓存 例如cache:shop: + idString json = stringRedisTemplate.opsForValue().get(key);// 2.判断缓存是否命中if (StrUtil.isNotBlank(json)) {// 2.1如果存在直接返回实体类信息t = JSONUtil.toBean(json, type);return t;}//现在json有2种可能,空字符串和nullif (null != json) {// 如果是空字符串,返回一个错误信息//return Result.fail("您要查询的店铺不存在!");return null;}// 1.1解决缓存击穿:尝试获取锁flag = tryLock(RedisConstants.LOCK_SHOP_KEY + id, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);// 1.2判断互斥锁是否获取成功if (!flag) {// 1.2.1获取失败 就休眠一会儿,然后查询Redis 的实体类Thread.sleep(50);}} while (flag == false);// 1.2.2获取成功 就直接进行下面的步骤// 2.2如果为null,就进行下面的操作// 2.2.1根据id查询数据库的实体类信息/*LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();// select * from tb_shop where id = ?wrapper.eq(Shop::getId, id);shop = shopMapper.selectOne(wrapper);*/t = function.apply(id);// TODO 模拟Redis击穿后,重建Redis的时间,可以去掉//Thread.sleep(200);// 2.2.2 如果不存在,直接返回错误if (null == t) {// 将空值写入redis并且设置有效期(缓存穿透)stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);//return Result.fail("您要查询的店铺不存在!");return null;}// 2.2.2 如果存在,先写入Redis然后返回String jsont = JSONUtil.toJsonStr(t);stringRedisTemplate.opsForValue().set(key, jsont);// 3.设置缓存的超时时间stringRedisTemplate.expire(key, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 4.释放锁unLock(RedisConstants.LOCK_SHOP_KEY + id);}// 5.返回return t;}

修改ShopServiceImpl.java

    @Overridepublic Result queryById(Long id) {// 解决缓存穿透//Shop shop = queryWithPassThough(id);// 解决缓存穿透(使用模板)//cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, id_key -> getById(id_key), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 解决缓存穿透(使用模板) 方法引用/*Shop shop = cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);*/// 互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);// 互斥锁解决缓存击穿Shop shop = cacheClient.queryWithMutex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 逻辑过期解决缓存击穿(使用模板)/*Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);*/if (null == shop) {return Result.fail("您要查询的店铺不存在!");}return Result.ok(shop);}

先用测试类给redis缓存中添加一条店铺信息,然后重复登录该id对应的网址,发现redis只会有一次sql的查询即可。
最后为了保证店铺信息的正常加载切换到
ShopServiceImpl.java

@Overridepublic Result queryById(Long id) {// 解决缓存穿透//Shop shop = queryWithPassThough(id);// 解决缓存穿透(使用模板)//cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, id_key -> getById(id_key), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 解决缓存穿透(使用模板) 方法引用Shop shop = cacheClient.queryWithPassThough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);// 互斥锁解决缓存击穿//Shop shop = cacheClient.queryWithMutex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);// 逻辑过期解决缓存击穿(使用模板)/*Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);*/if (null == shop) {return Result.fail("您要查询的店铺不存在!");}return Result.ok(shop);}

相关文章:

2022黑马Redis跟学笔记.实战篇(三)

2022黑马Redis跟学笔记.实战篇 三4.2.商家查询的缓存功能4.3.1.认识缓存4.3.1.1.什么是缓存4.3.1.2.缓存的作用1.为什么要使用缓存2.如何使用缓存3. 添加商户缓存4. 缓存模型和思路4.3.1.3.缓存的成本4.3.2.添加redis缓存4.3.3.缓存更新策略4.3.3.1.三种策略(1).内存淘汰:Redis…...

hadoop环境新手安装教程

1、资源准备&#xff1a; &#xff08;1&#xff09;jdk安装包&#xff1a;我的是1.8.0_202 &#xff08;2&#xff09;hadoop安装包&#xff1a;我的是hadoop-3.3.1 注意这里不要下载成下面这个安装包了&#xff0c;我就一开始下载错了 错误示例&#xff1a; 2、主机网络相…...

数据结构与算法基础-学习-11-线性表之链栈的初始化、判断非空、压栈、获取栈长度、弹栈、获取栈顶元素

一、个人理解链栈相较于顺序栈不存在上溢&#xff08;数据满&#xff09;的情况&#xff0c;除非内存不足&#xff0c;但存储密度会低于顺序栈&#xff0c;因为会多存一个指针域&#xff0c;其他逻辑和顺序表一致。总结如下&#xff1a;头指针指向栈顶。链栈没有头节点直接就是…...

Hive内置函数

文章目录Hive内置函数字符串函数时间类型函数数学函数集合函数条件函数类型转换函数数据脱敏函数其他函数用户自定义函数Hive内置函数 查询内置函数用法&#xff1a; DESCRIBE FUNCTION EXTENDED 函数名;字符串函数 字符串连接函数&#xff1a;concat带分隔符字符串连接函数…...

Git如何快速入门

什么是Git&#xff1f;我们开发的项目&#xff0c;也需要一个合适的版本控制系统来协助我们更好地管理版本迭代&#xff0c;而Git正是因此而诞生的&#xff08;有关Git的历史&#xff0c;这里就不多做阐述了&#xff0c;感兴趣的小伙伴可以自行了解&#xff0c;是一位顶级大佬在…...

netcore构建webservice以及调用的完整流程

目录构建前置准备编写服务挂载服务处理SoapHeader调用添加服务调用服务补充内容构建 前置准备 框架版本要求&#xff1a;netcore3.1以上 引入nuget包 SoapCore 编写服务 1.编写服务接口 示例 using System.ServiceModel;namespace Services;[ServiceContract(Namespace &…...

Mysql事务基础(解析)

并发事务带来的问题A和B是并发事务脏写&#xff08;A被B覆盖&#xff09;两个事务。B事务覆盖了A事务。解决&#xff1a;应该事务并行脏读&#xff08;B读到了A的执行中间结果&#xff09;A修改了东西。B看到了他的中间状态。解决&#xff1a;读写冲突。加锁&#xff0c;改完再…...

2023 年首轮土地销售活动来了 与 The Sandbox 一起体验「体素狂热」!

2 月 14 日晚上 11 点&#xff0c;开始你的体素冒险。 The Sandbox 很高兴推出 2023 年的第一次土地销售活动。欢迎来到「体素狂热 (Voxel Madness)」&#xff01; 简要概括 土地销售抽奖活动将于北京时间 2 月 14 日星期二晚上 11 点开始 「体素狂热」 土地销售活动将于 2 月…...

vue AntD中栅格布局的四种大小xs,sm,md,lg

cssBootstrap栅格布局的四种大小xs,sm,md,lg前端为了页面在不同大小的设备上也能够正常显示&#xff0c;通常会使用栅格布局的方式来实现。使用bootStrap的网格系统时&#xff0c;常见到一下格式的类名col-*-*visible-*-*hidden_*_* 中间可为xs,xsm,md,lg等表示大小的单词的缩写…...

window.open()打开窗口全屏

window.open (page.html, page, height100, width400, top0, left0, toolbarno, menubarno, scrollbarsno, resizableno,locationn o, statusno, fullscreenyes); 参数解释&#xff1a; window.open() 弹出新窗口的命令&#xff1b; ‘page.html’ 弹出窗口的文件名&#xff…...

VFIO软件依赖——VFIO协议

文章目录背景PCI设备模拟PCI设备抽象VFIO协议实验Q&A背景 在虚拟化应用场景中&#xff0c;虚拟机想要在访问PCI设备时达到IO性能最优&#xff0c;最直接的方法就是将物理设备暴露给虚拟机&#xff0c;虚拟机对设备的访问不经过任何中间层的转换&#xff0c;没有虚拟化的损…...

C/C++【内存管理】

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 Love is a choice. It is a conscious commitment. It is something you choose to make work every day with a person who has chosen the same thi…...

第8篇:Java编程语言的8大优势

目录 1、简单性 2、面向对象 3、编译解释性 4、稳健性 5、安全性 6、跨平台性...

STM32定时器实现红外接收与解码

1.NEC协议 红外遥控是一种比较常用的通讯方式&#xff0c;目前红外遥控的编码方式中&#xff0c;应用比较广泛的是NEC协议。NEC协议的特点如下&#xff1a; 载波频率为 38KHz8位地址和 8位指令长度地址和命令2次传输&#xff08;确保可靠性&#xff09;PWM 脉冲位置调制&#…...

18- Adaboost梯度提升树 (集成算法) (算法)

Adaboost 梯度提升树: from sklearn.ensemble import AdaBoostClassifier model AdaBoostClassifier(n_estimators500) model.fit(X_train,y_train) 1、Adaboost算法介绍 1.1、算法引出 AI 39年&#xff08;公元1995年&#xff09;&#xff0c;扁鹊成立了一家专治某疑难杂症…...

zlink 介绍

zlink 是一个基于 flink 开发的分布式数据开发工具&#xff0c;提供简单的易用的操作界面&#xff0c;降低用户学习 flink 的成本&#xff0c;缩短任务配置时间&#xff0c;避免配置过程中出现错误。用户可以通过拖拉拽的方式实现数据的实时同步&#xff0c;支持多数据源之间的…...

C++之std::string的resize与reverse

std::string的resize与reverse前言1.resize2.reserve前言 在C中我们经常用std::string 来保存字符串&#xff0c;其中有两个比较常用但是却平时容易被搞混的两个函数&#xff0c;分别是resize和reserve&#xff0c;模糊意识里&#xff0c;这两个方法都是对std::string的容量或元…...

在.net中运用ffmpeg 操作视频

using System;using System.Collections.Generic;using System.Diagnostics;using System.IO;using System.Text;namespace learun.util{/// <summary>/// ffmpeg视频相关处理的类/// </summary>public class FFmpegUtil{public static int Run(string cmd){try{//…...

05- 线性回归算法 (LinearRegression) (算法)

线性回归算法(LinearRegression)就是假定一个数据集合预测值与实际值存在一定的误差, 然后假定所有的这些误差值符合正太分布, 通过方程求这个正太分布的最小均值和方差来还原原数据集合的斜率和截距。当误差值无限接近于0时, 预测值与实际值一致, 就变成了求误差的极小值。 fr…...

JAVA补充知识01之枚举enum

目录 1. 枚举类的使用 1.1 枚举类的理解 1.2 举例 1.3 开发中的建议&#xff1a; 1.4 Enum中的常用方法 1.5 熟悉Enum类中常用的方法 1.6 枚举类实现接口的操作 1.7 jdk5.0之前定义枚举类的方式 &#xff08;了解即可&#xff09; 1.8 jdk5.0之后定义枚举类的方式 1…...

从零实现富文本编辑器#5-编辑器选区模型的状态结构表达

先前我们总结了浏览器选区模型的交互策略&#xff0c;并且实现了基本的选区操作&#xff0c;还调研了自绘选区的实现。那么相对的&#xff0c;我们还需要设计编辑器的选区表达&#xff0c;也可以称为模型选区。编辑器中应用变更时的操作范围&#xff0c;就是以模型选区为基准来…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

基于当前项目通过npm包形式暴露公共组件

1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹&#xff0c;并新增内容 3.创建package文件夹...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

Java面试专项一-准备篇

一、企业简历筛选规则 一般企业的简历筛选流程&#xff1a;首先由HR先筛选一部分简历后&#xff0c;在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如&#xff1a;Boss直聘&#xff08;招聘方平台&#xff09; 直接按照条件进行筛选 例如&#xff1a…...

【Oracle】分区表

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统

目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索&#xff08;基于物理空间 广播范围&#xff09;2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)

上一章用到了V2 的概念&#xff0c;其实 Fiori当中还有 V4&#xff0c;咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务)&#xff0c;代理中间件&#xff08;ui5-middleware-simpleproxy&#xff09;-CSDN博客…...