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

【 Redis | 实战篇 缓存 】

目录

前言:

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

==》返回商铺数据

@Autowiredprivate StringRedisTemplate stringRedisTemplate; 
@Overridepublic 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.查询RedisString 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.存在,存入RedisString 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,并且设置过期时间(过期淘汰)

==》返回商铺数据

@Autowiredprivate StringRedisTemplate stringRedisTemplate; 
@Overridepublic 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.查询RedisString 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.存在,存入RedisString 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,并且设置过期时间(过期淘汰)

==》返回商铺数据

@Autowiredprivate StringRedisTemplate stringRedisTemplate; 
@Overridepublic 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.查询RedisString 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.存在,存入RedisString jsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return shop;}

写操作:

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

例子:

 @Override@Transactionalpublic 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.删除RedisstringRedisTemplate.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,并且设置过期时间(过期淘汰)

==》返回商铺数据

 @Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic 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.查询RedisString 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.存在,存入RedisString 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缓存操作阶段

 

@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryShopById(Long id) {//互斥锁缓存击穿Shop shop = queryShopBreakdown(id);if (shop == null){return Result.fail("商铺不存在");}//返回商铺数据return Result.ok(shop);}//基于互斥锁,击穿public Shop queryShopBreakdown(Long id) {//1.查询RedisString 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.存在,存入RedisString 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直接返回旧商铺数据

 ​​​​​​​

 @Autowiredprivate StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);@Overridepublic Result queryShopById(Long id) {//逻辑Shop shop = queryExpireTime(id);if (shop == null){return Result.fail("商铺不存在");}//返回商铺数据return Result.ok(shop);}//逻辑public Shop queryExpireTime(Long id) {//1.查询RedisString 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;}//存入逻辑Redispublic void expireTime(Long id,Long expire){//根据id查询数据库Shop shop = getById(id);//存入RedisRedisData 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);//穿透,写入Redisprivate void set(Long time, TimeUnit unit, String key, Object value) {String jsonStr = JSONUtil.toJsonStr(value);stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);}//击穿,写入Redisprivate 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.查询RedisString 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.存在,存入Redisthis.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.查询RedisString 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);//存入Redisthis.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以及泛型函数

相关文章:

【 Redis | 实战篇 缓存 】

目录 前言&#xff1a; 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.介绍 …...

2025年全新 GPT 4.5 AI 大模型 国内免费调用

一、中转账号注册 第一步&#xff1a;打开宙流AI中转站&#xff0c;网站地址如下&#xff1a; 宙流AI中转站 按照上图中的操作步骤&#xff0c;通过邮箱进行账号注册&#xff0c;注册完毕后&#xff0c;网站初始会分配0.4刀的免费额度&#xff0c;获取额度后&#xff0c;即可…...

“睿思 BI” 系统介绍

“睿思 BI” 商业智能系统是由成都睿思商智科技有限公司自主研发的企业数据分析系统&#xff0c;以下是对该系统的详细介绍&#xff1a; 功能模块 &#xff1a; • 数据集成与准备 &#xff1a;支持数据导入、数据填报、数据 ETL 等功能&#xff0c;可抽取企业在经营过程中产生…...

《C++探幽:模板从初阶到进阶》

文章目录 :red_circle:一、模板基础&#xff1a;开启泛型编程之门&#xff08;一&#xff09;泛型编程的必要性&#xff08;二&#xff09;函数模板1. 函数模板概念2. 函数模板定义格式3. 函数模板原理4. 函数模板实例化5. 模板参数匹配原则 &#xff08;三&#xff09;类模板1…...

【Elasticsearch】在kibana中能获取已创建的api keys吗?

在 Kibana 中&#xff0c;目前没有直接的界面功能可以列出或查看已创建的 API 密钥&#xff08;API keys&#xff09;。 API 密钥的管理和查看主要通过 Elasticsearch 的 REST API 来完成&#xff0c;而不是通过 Kibana 的管理界面。 在 Kibana 中使用 Dev Tools 查看 API 密钥…...

Spring事务管理实现机制

Spring通过一系列精妙的抽象和实现来完成事务的融入、挂起和嵌套操作。下面我将详细解析Spring如何实现这些事务行为。 1. 核心组件 Spring事务管理的核心组件包括&#xff1a; PlatformTransactionManager&#xff1a;事务管理器的抽象接口 TransactionDefinition&#xff…...

[面试]SoC验证工程师面试常见问题(七)低速接口篇

SoC验证工程师面试常见问题(七)低速接口篇 摘要:低速接口是嵌入式系统和 SoC (System on Chip) 中常用的通信接口,主要用于设备间的短距离、低带宽数据传输。相比高速接口(如 PCIe、USB 3.0),低速接口的传输速率较低(通常在 kbps 到几 Mbps 范围),但具有简单…...

虚假AI工具通过Facebook广告传播新型Noodlophile窃密木马

网络安全公司Morphisec的研究人员发现&#xff0c;攻击者正利用虚假人工智能&#xff08;AI&#xff09;平台传播名为Noodlophile Stealer的新型信息窃取木马。这种复杂攻击手法利用AI工具的热度诱骗用户下载恶意软件&#xff0c;窃取浏览器凭证、加密货币钱包&#xff0c;并可…...

【Qt】之【Bug】点击按钮(ui->pushButton)触发非本类设置的槽函数

解决 先说解决办法&#xff0c;按钮在ui为默认命名ui->pushButton,后面改了下按钮名为该按钮的功能相关&#xff0c;就不会随意触发其他槽函数了。 没想到是这个原因。。。 可能是之前默认的objectName与旧的槽函数自动连接了 记录一下&#xff0c;找了好久其他的原因。 以…...

Metasploit 4.22.7:企业级渗透测试新突破

前言 Metasploit作为全球最受欢迎的渗透测试框架,其最新版本4.22.7-2025050101带来了企业级开发的全新可能。 本文将从框架基础架构、模块类型与开发规范入手,逐步深入企业级功能如MetaModules任务重放和自动化测试,最终通过实战案例展示如何利用最新功能开发高效漏洞利用模…...

【LeetCode 热题 100】215. 数组中的第K个最大元素(Python 快速选择详解)

在刷 LeetCode 的过程中&#xff0c;“第K大”是一个非常高频的考点&#xff0c;而题目 215. 数组中的第K个最大元素 就是经典代表。这道题不仅考察我们对排序的理解&#xff0c;还挑战我们写出时间复杂度为 O(n) 的算法。 本文将带你深入理解并实现一个基于快速选择&#xff…...

麦科信获评CIAS2025金翎奖【半导体制造与封测领域优质供应商】

在苏州举办的2025CIAS动力能源与半导体创新发展大会上&#xff0c;深圳麦科信科技有限公司凭借在测试测量领域的技术积累&#xff0c;入选半导体制造与封测领域优质供应商榜单。本届大会以"新能源芯时代"为主题&#xff0c;汇集了来自功率半导体、第三代材料应用等领…...

指针运算典型例题解析

1.题目1 该代码运行的结果是什么&#xff1f; #include <stdio.h> int main() { int a[5] { 1, 2, 3, 4, 5 }; int *ptr (int *)(&a 1); printf( "%d,%d", *(a 1), *(ptr - 1)); return 0; } 解析&#xff1a; 运行结果&#xff1a; 2.题目2 在X86…...

DAX 权威指南1:DAX计算、表函数与计算上下文

参考《DAX 权威指南 第二版》 文章目录 二、DAX简介2.1 理解 DAX 计算2.2 计算列和度量值2.3 变量2.3.1 VAR简介2.3.2 VAR的特性 2.4 DAX 错误处理2.4.1 DAX 错误类型2.4.1.1 转换错误2.4.1.2 算术运算错误2.4.1.3 空值或 缺失值 2.4.2 使用IFERROR函数拦截错误2.4.2.1 安全地进…...

使用 NV‑Ingest、Unstructured 和 Elasticsearch 处理非结构化数据

作者&#xff1a;来自 Elastic Ajay Krishnan Gopalan 了解如何使用 NV-Ingest、Unstructured Platform 和 Elasticsearch 为 RAG 应用构建可扩展的非结构化文档数据管道。 Elasticsearch 原生集成了行业领先的生成式 AI 工具和提供商。查看我们的网络研讨会&#xff0c;了解如…...

20250508在WIN10下使用移远的4G模块EC200A-CN直接上网

1、在WIN10/11下安装驱动程序&#xff1a;Quectel_Windows_USB_DriverA_Customer_V1.1.13.zip 2、使用移远的专用串口工具&#xff1a;QCOM_V1.8.2.7z QCOM_V1.8.2_win64.exe 3、配置串口UART42/COM42【移远会自动生成连续三个串口&#xff0c;最小的那一个】 AT命令&#xf…...

C++(6):逻辑运算符

目录 1. 代码示例 示例 1&#xff1a;基础用法 示例 2&#xff1a;条件判断 2. 短路求值&#xff08;Short-Circuit Evaluation&#xff09; 代码示例 3. 实际应用场景 场景 1&#xff1a;输入合法性验证 场景 2&#xff1a;游戏状态判断 4. 注意事项 逻辑运算符用于组…...

NXP iMX8MP ARM 平台多屏幕克隆显示测试

By Toradex秦海 1). 简介 NXP i.MX8MP ARM SoC 支持 3 路 Display Controller 分别提供 DSI/HDMI/LVDS 显示输出&#xff0c;在 Yocto Linux BSP 下采用 Wayland Backend 基于 DRM subsystem 显示驱动&#xff0c;前端默认基于 Weston Compositor。因此在默认情况下连接多个屏…...

探秘 Canva AI 图像生成器:重塑设计创作新范式

Canva 凭借简洁易用的界面和海量模板资源&#xff0c;早已成为设计师和普通用户的心头好。而 Canva AI 图像生成器的推出&#xff0c;更是为设计领域带来了一场深刻变革&#xff0c;以智能化的手段重塑了图像创作的方式与边界。 技术内核&#xff1a;AI 如何驱动图像生成 Can…...

【数据结构】——栈

一、栈的概念和结构 栈其实就是一种特殊的顺序表&#xff0c;其只允许在一端进出&#xff0c;就是栈的数据的插入和删除只能在一端进行&#xff0c;进行数据的插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的元素遵循先进后出LIFO&#xff08;Last InFirst O…...

Navicat中保存的数据库密码找回 Java 8

导出数据库连接打开导出的connections.ncx文件找到加密的password放入java程序中解密即可 package com.asia.card.cloud.enterprise.api;import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.cha…...

构件是一个逻辑概念,还是一个物理概念?

在软件架构中,​​构件(Component)​​既可以是逻辑概念,也可以是物理概念,具体取决于上下文和系统设计的需求。以下是两种视角的详细分析: ​​1. 逻辑概念(抽象层面)​​ ​​定义​​:构件是系统功能的逻辑划分,表示一组相关的职责或行为,不直接对应物理实现。 ​…...

vs code管理员权限启动问题

vs code非管理员启动可以正常启动用管理员启动vs code&#xff0c;会提示 解决办法 找到argv.json文件在argv.json文件中添加 "disable-chromium-sandbox": true重启vs code即可...

Spring Cloud与Service Mesh集成:Istio服务网格实践

文章目录 引言一、Spring Cloud与Service Mesh概述二、Istio服务网格架构三、Spring Cloud与Istio集成的基础设施准备四、服务发现与负载均衡五、流量管理与弹性模式六、安全通信与认证授权七、可观测性集成八、配置管理集成总结 引言 微服务架构已成为现代分布式系统的主流设…...

20250510-查看 Anaconda 配置的镜像源

打开 Anaconda Prompt 查看 Anaconda 当前配置的镜像源&#xff0c;使用命令 conda config --show channels这将显示当前配置的通道&#xff08;channels&#xff09;&#xff0c;即镜像源列表。 此外&#xff0c;还可以使用 conda config --show命令来显示conda的配置信息&…...

React+Taro选择日期组件封装

话不多说&#xff0c;直接上效果 1.页面渲染时间模块 {this.renderCalendarPopup()}2.引入时间组件弹层&#xff0c;state中加入showPopup(控制什么时候展示时间选择弹层)&#xff0c;time(选择后的时间值) private renderCalendarPopup () > {const { showPopup, time…...

C++进阶--AVL树的实现续

文章目录 C进阶--AVL树的实现双旋AVL树的查找AVL树的检验结语 很高兴和搭大家见面&#xff0c;给生活加点impetus&#xff0c;开启今天的比编程之路&#xff01;&#xff01; 今天我们来完善AVL树的操作&#xff0c;为后续红黑树奠定基础&#xff01;&#xff01; 作者&#x…...

AutoGen+Deepseek+chainlit的简单使用

AutoGen 的应用场景 AutoGen 作为一个强大的多智能体协作框架&#xff0c;可用于多种复杂任务&#xff1a; 自动化工作流&#xff1a;构建由多个智能体组成的流水线&#xff0c;例如数据收集、分析、报告生成复杂问题分解&#xff1a;将难题拆解为子任务&#xff0c;分配给不…...

采用SqlSugarClient创建数据库实例引发的异步调用问题

基于SqlSugar编写的多个WebApi接口&#xff0c;项目初始化时采用单例模式注册SqlSugarClient实例对象&#xff0c;前端页面采用layui布局&#xff0c;并在一个按钮事件中通过Ajax连续调用多个WebApi接口获取数据。实际运行时点击按钮会随机报下面几种错误&#xff1a; Execute…...

第7次课 栈A

课堂学习 栈&#xff08;stack&#xff09; 是一种遵循先入后出逻辑的线性数据结构。 我们可以将栈类比为桌面上的一摞盘子&#xff0c;如果想取出底部的盘子&#xff0c;则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素&#xff08;如整数、字符、对象等&…...