缓存相关问题记录解决

缓存相关问题

在这里我不得不说明,我写的博客都是我自己用心写的,我自己用心记录的,我写的很详细,所以会有点冗长,所以如果你能看的下去的化,会有所收获,我不想写那种copy的文章,因为对我来说没什么益处,我写的这篇博客,就是为了记录我缓存的相关问题,还有我自己的感悟,所以如果你有耐心看下去,我希望和你交朋友,如果你觉得我哪些地方写的不正确,你可能立马私信我,或者评论,希望你能参与到我的博客写作中来

缓存更新策略

缓存更新有三种策略

在这里插入图片描述

第一种,是类似redis这种缓存自己内置的内存淘汰机制
第二种,是redis的ttl,这个也很好理解
第三种,是我们再修改涉及到缓存的数据的时候,主动去更新相关缓存

可想而知,前两种,都不是可控的,我们实际上再项目中,我们应该都一起用,redis的内存淘汰做次要,还有ttl也做次要,主要我们要写的就是这个主动更新,这样才很好的保持一致性

主动更新策略

主动更新也有三种策略
在这里插入图片描述
看上去,有点难理解,其实很好理解后两种,就是依赖于别的服务,去进行缓存更新,第一种,是我们程序员自己去更新

像后两种,我就想起一个框架,SpringCache 这样的框架,你说它好吧,确实也挺好,能省写很多代码,但是他最大的缺点就在于,不灵活,不可控,总的来看,也没省多少代码,我认为不必去想后两种的方式,除非你公司要用,我门应该选择最可控的,也就是自己写!

缓存与数据一致性解决

当数据库发生更新的时候,我们有几个问题需要去考虑

到底是更新缓存,还是删除缓存

第一个问题就是到底是更新好,还是直接删除好,这里我们想想也是还是删除好,不能每次都去更新,这样多浪费资源啊
我们应该直接删除,然后下一个人来查询的时候,再去更新缓存

如何保证缓存与数据库同时失败和成功

单体项目: 加上事务@Transactional
分布式系统,利用tcc等分布式解决方案

我们到底是先删除缓存还是先更新数据库

这个问题很值得去研究一下,如果你想研究明白,就得去画个图,看看,那个比较好,我们先说结论,先更新数据库,一致性会比较好,我这里写的会写的十分详细,我认为这里很有意思,希望你能看下来,你会觉得先更新数据库会更合理一点

你可能会不服,但是你先听我讲,我把工作的线程叫做更新线程,
扰乱我们工作的,我叫他捣乱线程

假设我们先删除缓存

在这里插入图片描述
上面这个图就是会发生得到缓存是不一致的

我们来研究这个问题,就得分为三个时间点
第一: 如果是在删除缓存之前,

  1. 查询缓存,得到的就是旧数据
  2. 删除缓存 ,此时缓存为空
  3. 更新数据库
    此时缓存为空,下一次查询的时候,得到正确的数据,这是正确的

第二: 如果我们在删除缓存之后,更新数据库之前,

  1. 更新线程先删除缓存
  2. 捣乱线程先查缓存,没有命中,查询数据库
  3. 捣乱线程写入缓存,此时的缓存是旧数据
  4. 最后再更新数据库
    我们能看到,此时缓存中的数据和数据库不一致,出现一致性问题!

第三: 如果我们再更新数据库之后,来查的缓存,
此时的缓存直接就是空的,那么我们捣乱线程来查的化,会去数据库查到正确的数据,此时是正确的

总结来看,就是当删除缓存之后,更新数据库之前,来了一个查询,就会出现一致性问题,而且可想而知,如果并发量大的化,很容易出现这种问题,因为这两个操作中间的时间太久了,很容易出问题

假设我们先更新数据库

在这里插入图片描述
上面这个图就是有可能会发生错误的时机,这里你看到可能会有问题,为什么这里查缓存会直接查不到呢? 你想,
但是确实如果会出现一致性问题的化,有一个大前提,就是再更新数据库之前,我们的缓存就出错了或者失效

我们一步一步来看,假设没有这个前提,也就是说,如果缓存没有失效

第一: 再更新数据库之前,

  1. 查缓存,此时缓存是旧数据,
  2. 然后更新数据库
  3. 删除缓存

此时缓存是空的,那么下一次查询就可以得到正确的数据,没问题

第二: 我们再更新数据库之后,删除缓存之前

  1. 查缓存,得到的是旧数据
  2. 然后删除缓存

此时缓存还是空的,所以下一次查询还是正确的数据,没问题

第三: 我们在删除缓存之后,查数据,这个时候,肯定是得到新的数据,这也是没什么问题的

所以,综上所述,在我们更新的时候,假设缓存还存在,那么就不会出现一致性问题

那么你就想知道了,那么什么时候会出现一致性问题呢?

出现这个一致性问题,有两个前提
第一: 也是上面我们论证的,就是必须在更新数据库的时候,缓存突然失效了
第二: 我们并行过来的查缓存,必须写入缓存在删除缓存之后

你们可能会不是很理解我这里的化,那么就得出现我上面那个图了
其实
其实也很好理解,左边这个线程是来捣乱的那个线程,右边的线程是我们更新的线程

我上面的第二个前提说的就是,这里的第3步删除缓存必须在写入缓存之前

我们如何论证呢?
我们假设这里删除缓存在写入缓存之后,会发生什么事情

那么整体的流程就是这样,

  1. 捣乱线程先查缓存,因为缓存失效,没有命中,查数据库
  2. 我们的更新线程先去更新数据库
  3. 捣乱线程,写入缓存,此时的缓存是旧的
  4. 我们的更新线程删除缓存,此时缓存为空

我们捋了一下这个过程,会发现,此时依然是正确的,删除缓存在最后,得到的缓存就会变成空,没有一致性问题!

最终出现问题的时机!!!

我们继续来捋一下这里的过程

  1. 首先捣乱线程先查询缓存,此时由于缓存失效,所以未命中,查询数据库,此时的到的是旧数据
  2. 更新线程,更新数据库
  3. 更新线程删除缓存,此时缓存为空
  4. 捣乱线程写入缓存,此时写入的是旧数据

那么,就终于出现一致性问题了,此时得到的就是旧数据

最终比较

那么我想你看明白我想说的,就很明了了,为什么我们要去先更新数据库?
问题的关键就在于,哪种情况更容易出问题,那么先删除缓存,出问题的几率更大,而先更新数据库,出问题的几率很小,因为我们要满足两大前提
第一个前提是,更新数据库之前,缓存莫名其妙不见了
第二个前提是,捣乱线程写入缓存的时候,是在更新线程删除缓存之后

这个条件是很严苛的,所以最后的答案就是先更新数据库!

缓存穿透

什么是缓存穿透,很好理解,就是缓存没命中,数据库没命中,这样所以类似的查询全部达到数据库上,那么数据库就爆炸了

如果有一个黑客知道你有缓存穿透的问题的化,那么他就打很多的请求达到你这个系统里边,那么你系统就宕机了

解决办法,有两个,我比较能理解第一个,第二个不太了解,等我了解了,我再来更新这篇博客

缓存空对象

在这里插入图片描述
第一种,也是耳熟能祥的,也就是缓存一个空对象过来,他的解决思路其实也很好理解,不是说redis缓存中不了吗,那么我们就让他中缓存,如果说redis中,没查到,数据库中没查到,我们就设置一个空对象,
key是刚刚查询的key,只不过value是null,那么就算他有几亿次请求,也都是命中缓存,打不到数据库

缺点

你会觉得啊,这个解决很好啊,那么就可以杜绝所有缓存穿透的问题了,不对,只要黑客换个思路的化,那么一样会有问题

如果说黑客,知道你有设置空对象来防御缓存穿透,那么他就换个思路,既然你设置空对象,那么我让你把redis内存全都挤满空对象,那么你redis最后也是宕机,整个服务还是宕机!

所以,搞空对象会有内存占用,我们一般得设置ttl来防御此类情况发生
而且这里的ttl不能设置太久,如果设置太久一样会出现这个问题

总结来看缺点就是:

  • 有额外的内存消耗,一般设置ttl
  • 可能会造成短期的不一致,设置的ttl要合理,太久了不行

布隆过滤

在这里插入图片描述
这里的布隆过滤器,我也不是很明白,他的判断依据是什么,但是我们能理解他的设计思路,就是再加一层来保存缓存,如果没有命中,就拦截

缓存空对象实战

接下来我们来实现缓存空对象,我们先来看原来的我这里的示例流程,我这的实战也是有些复杂,希望你不要那么着急,这里的数据库中的表,你可以随意写一个,只要能返回列表的,我这里的表是商铺数据

在这里插入图片描述
整体的流程我简单的概述一下,看下来就是很简单的缓存商户的信息,先是去判断缓存中是否有,如果没有就去查数据库

原先的代码

Controller

    @GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {Shop shop = shopService.queryShopById(id);return Result.ok(shop);}

IShopService

public interface IShopService extends IService<Shop> {/*** 获取商户信息* @param id* @return*/Shop queryShopById(Long id);
}

实现类

    /*** 获取商户信息* @param id* @return*/@Overridepublic Shop queryShopById(Long id) {//查redisString shopKey = RedisConstants.CACHE_SHOP_KEY + id;String shopJSON = redisCache.getCacheObject(shopKey);//缓存有,直接返回Shop shop = null;if(StrUtil.isNotBlank(shopJSON)) {shop = JSONUtil.toBean(shopJSON,Shop.class);return shop;}//不存在就去查数据库shop = getById(id);//数据库没查到!if(Objects.isNull(shop)) {return null;}//存入缓存redisCache.setCacheObject(shopKey,shop);return shop;}

问题复现
我数据库中,没有id为15的商铺数据,这里的示范数据,只要选你表中没有的进行测试

发送请求
在这里插入图片描述
不断的发几次请求,看idea的sql是否有几段
在这里插入图片描述
结果确实是重复的

代码

其他的都基本差不多,这里我就只贴出,service是实现类的改动代码

/*** 获取商户信息* @param id* @return*/
@Override
public Shop queryShopById(Long id) {//查redisString shopKey = RedisConstants.CACHE_SHOP_KEY + id;String shopJSON = redisCache.getCacheObject(shopKey);//缓存有,直接返回Shop shop = null;if(StrUtil.isNotBlank(shopJSON)) {shop = JSONUtil.toBean(shopJSON,Shop.class);return shop;}//判断是否是我们自己写的""if(shopJSON != null) {return null;}//不存在shop = getById(id);if(Objects.isNull(shop)) {//导入空值,进入缓存redisCache.setCacheObject(shopKey,"",RedisConstants.CACHE_NULL_TTL.intValue(), TimeUnit.MINUTES);return null;}//存入缓存redisCache.setCacheObject(shopKey,shop);return shop;
}

改动的地方
在这里插入图片描述

测试
在这里插入图片描述

这里只会触发一次,不管请求多少次,但是ttl一过,还会发一次

缓存雪崩

缓存雪崩很好理解,什么是雪崩,就是突然很多雪突然松动,最后一起崩坏
所以缓存雪崩出现的原因就是,同一时间段,大量的缓存key同时失效,或者说redis宕机,那么大量请求打到数据库,数据库就爆炸了!

解决起来也不是特别难,主要是我们的系统得健壮一点,不能这么脆弱

解决方法

  • 给不同的key的ttl添加随机值
  • redis集群
  • 给缓存添加降级限流的策略
  • 给业务设置多级缓存

所以我们要么多搞点redis,多加几层缓存,这样的问题,也是很容易避免的

缓存击穿

缓存击穿,这里的击穿是由于热点key的问题,热点key突然集体失效,那么 高并发 + 缓存重建业务复杂 ,无数的请求打到数据库,那么数据库就爆炸了!
在这里插入图片描述

这里的缓存击穿,更形象的说,是一瞬间事,他的意思是在高并发的那一个瞬间,突然缓存失效,加上缓存重建要很久,所以就爆炸了

有两种解决方法

互斥锁

出现缓存击穿问题就在于,在那一瞬间有很多重建的请求,那么我们就消除那么多重建的请求不就的了,那么就很容易想到,加锁,当发现要重建的时候,第一个请求就加上锁,之后再来请求就获取锁失败,让他休眠一会,再重试
在这里插入图片描述

逻辑过期

逻辑过期的想法,还挺有想法的,就是设置一个逻辑过期的字段在这里插入图片描述

逻辑过期我认为他最大的好处就是,不用等,我们互斥锁的化,就会去等,性能不是特别好,那么这里就不用去等,但是这里就会有一致性问题,当然按理来说,这里的重建key的时间,要是不是很久的化,那么这里的一致性问题也不会那么大

这里就像一个悖论,你要性能好,一致性就会有瑕疵,你要一致性好,性能就没那么好,但是按理来说,以现在的要求来看,我觉得性能应该更追求一点,所以逻辑过期的市场会大一点

比较

在这里插入图片描述

互斥锁实战

接下来的实战,就是模拟高并发下的缓存击穿问题,会比较复杂,所以需要仔细看,但是如果你做完了我这个实验,会对缓存击穿的解决会理解很多,毕竟计算机是实操大于理论

我们先来看,如何复现缓存击穿问题,
在这里我得多说一句,我们一定要自己复现这些问题,因为如果你只是学习怎么解决的化,那永远是一知半解,所以我认为只有了解敌人,才能更好的打倒敌人!

问题复现

首先,就是高并发,第二就是缓存的key在高并发的哪一个瞬间失效了
所以我们得着手准备这两个方面

第一,高并发,我门用著名的压测工具,Jmeter来实现,Jmeter的使用,我这里就不多说了,你不愿意学,看着我的操作,也能直接用~

Jmeter

首先创建一个线程组,然后创建一个http请求,并且在这个请求下,打开结果树
在这里插入图片描述
在这里插入图片描述

创建一个http请求
在这里插入图片描述

写好我们要测试的接口
在这里插入图片描述

调出结果树
在这里插入图片描述

ok,jmeter准备就绪

出问题的代码

我这里的代码不是很复杂,是正常的缓存商户信息

  • controller
@RestController
@RequestMapping("/shop")
public class ShopController {@Resourcepublic IShopService shopService;/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {Shop shop = shopService.queryShopById(id);if(shop == null) {return Result.fail("店铺信息不存在!");}return Result.ok(shop);}}
  • 接口抽象类
public interface IShopService extends IService<Shop> {/*** 获取商户信息* @param id* @return*/Shop queryShopById(Long id);
}

实现类

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Autowiredprivate RedisCache redisCache;@Autowiredprivate IShopTypeService shopTypeService;/*** 获取商户信息* @param id* @return*/@Overridepublic Shop queryShopById(Long id) {Shop shop = null;//出问题的代码,这个代码,也是从上面的缓存穿透继承过来的shop = queryWithPassThrough(id);return shop;}/*** 解决缓存穿透 --> 缓存空对象* @param id* @return*/public Shop queryWithPassThrough(Long id) {//查redisString shopKey = RedisConstants.CACHE_SHOP_KEY + id;String shopJSON = redisCache.getCacheObject(shopKey);//缓存有,直接返回Shop shop = null;if(StrUtil.isNotBlank(shopJSON)) {shop = JSONUtil.toBean(shopJSON,Shop.class);return shop;}//判断是否是我们自己写的""if(shopJSON != null) {return null;}//不存在shop = getById(id);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if(Objects.isNull(shop)) {//导入空值,进入缓存redisCache.setCacheObject(shopKey,"",RedisConstants.CACHE_NULL_TTL.intValue(), TimeUnit.MINUTES);return null;}String json = JSON.toJSONString(shop);//存入缓存redisCache.setCacheObject(shopKey,json);return shop;}
}

我这里再来简单说一下我这里的出问题的代码的逻辑,好让你理清楚

  1. 查商户的redis缓存
  2. 如果有,直接返回,(但是这里为了出问题,我这里把redis缓存清空)
  3. 如果没有,就进行缓存重建,重建的过程就是查数据库 + 存入redis

总体就是这么个流程

测试

我们有几个要注意的点,
第一,redis应该是没有这个shop的缓存的
第二我们要在心里知道,应该出现什么结果,这里应该出现的结果就是,再高并发的情况下,因为缓存重建化的时间有点久,所以会有很多请求打到数据库,所以我们得着眼观看idea中控制台的消息,如果出现很多sql打到数据库,说明问题出现了
在这里插入图片描述
启动!!!

在这里插入图片描述

成功,问题出现了,我这里展现不权,实则有很多的请求,所以这就是高并发下,出现的这种缓存击穿问题

解决

为了解决这个问题,我们得考虑如何实现这个互斥锁,那么这个时候,你就会想,这还不简单?,直接再后端代码中,写一个锁的代码不就行了吗?
这就是你考虑的不周到了,如果是两个端的人都在请求这个接口呢?那不还是有问题,所以我们得把这个锁抽离出来,那么redis实现互斥锁,就呼之欲出了!

可能你想问,redis怎么实现互斥锁? 很简单,setnx,setnx这个命令是只有存在这个key的时候,才会set成功,如果不存在,就失败

所以,我们要设置锁,就setnx,如果setnx失败,那么久说明有人占用着锁
那么如何释放锁呢,也是很简答,直接删除这个key value,就相当于释放锁了

但是有一件事情我们必须注意!,那就是这里的锁,一定要设置过期时间,我们这种小测试还好,如果去到很大的体量的系统里边不设置过期时间,有可能会有死锁问题,或者其他异常,这里是给我们自己留一个后路

但是一件事情就是有利有弊,这里我们虽然溜了一个后路,但是由于这里设置了过期时间,后序还可能会出现其他问题,这里的问题也是后话了,我们先不考虑

准备

首先我们先封装获取锁 + 释放锁的代码
我这里把他封装在我的工具类里边了

    /*** 尝试获取锁* @param pattern key* @param value 值* @param timeout 过期时间* @param timeUnit 时间单位* @param <T>* @return*/public <T> boolean tryLock(String pattern,T value,Long timeout,TimeUnit timeUnit){Boolean flag = redisTemplate.opsForValue().setIfAbsent(pattern, value, timeout, timeUnit);return BooleanUtil.isTrue(flag);}/*** 尝试获取锁* @param pattern key* @param value 值* @param <T>* @return*/public <T> boolean tryLock(String pattern,T value){Boolean flag = redisTemplate.opsForValue().setIfAbsent(pattern, value, 2, TimeUnit.MINUTES);return BooleanUtil.isTrue(flag);}/*** 解锁* @param pattern*/public void unlock(String pattern) {//删除锁redisTemplate.delete(pattern);}
}

你想放在哪都行
这里我特意我设置了两个tryLock,一个是有写过期时间的,一个是默认写过期时间的,你应该也能看懂

改造代码

我们先来看到底该如何改造,这里我们来看一个流程图,看着流程图再去改造自己的代码
在这里插入图片描述
我们写这种比较复杂的业务代码的时候,还是有必要画一个流程图,这样我们的方向会更具体!

核心改造代码

    /*** 解决缓存击穿 --> 互斥锁* @param id* @return*/public Shop queryWithMutex(Long id) {//查redisString shopKey = RedisConstants.CACHE_SHOP_KEY + id;String shopJSON = redisCache.getCacheObject(shopKey);//缓存有,直接返回Shop shop = null;if(StrUtil.isNotBlank(shopJSON)) {shop = JSONUtil.toBean(shopJSON,Shop.class);return shop;}//判断是否是我们自己写的""if(shopJSON != null) {return null;}try {//尝试第一次获取锁boolean isLock = redisCache.tryLock(RedisConstants.LOCK_SHOP_KEY, "1");//没有获取到锁,休眠一段时间if(!isLock) {//休眠try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//重试,递归,休眠一段时间,看是否能拿到缓存,还拿不到继续获取锁,看看自己能不能重建return queryWithMutex(id);}//不存在shop = getById(id);if(Objects.isNull(shop)) {//导入空值,进入缓存redisCache.setCacheObject(shopKey,"",RedisConstants.CACHE_NULL_TTL.intValue(), TimeUnit.MINUTES);return null;}//存入缓存redisCache.setCacheObject(shopKey,JSON.toJSONString(shop));} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁redisCache.unlock(RedisConstants.LOCK_SHOP_KEY);}return shop;}

我这里再来解读一下这里的代码

  1. 首先是读缓存,如果有,直接返回缓存(但是这里我们为了测试,是把缓存删了的)
  2. 如果没有缓存,接下来,第一次获取锁
  3. 获取锁成功,便开始重建缓存,这里是正常的业务代码
  4. 获取锁失败,便睡几秒,然后递归调用这里的queryMutex(id)

我认为这里的递归调用自己还算是巧妙,我本来的想法是再去获取锁,写一个循环,这样完全是错误的
因为获取锁之后,还是去重建缓存,这里应该是去再去查缓存是否有,这样才对,所以我犯了这个错误的原因就是太像当然,应该想清楚自己应该干的事!

测试

这里是最终的测试
首先,redis得清空,不能有缓存
然后就是操作jmeter

这里的操作jmeter和我再问题复现的那里写的是一样的,所以我这里就不赘述了

    /*** 获取商户信息* @param id* @return*/@Overridepublic Shop queryShopById(Long id) {Shop shop = null;//缓存穿透shop = queryWithMutex(id);return shop;}

调用我们新写的代码

redis也是空的
在这里插入图片描述

idea的控制台清空
在这里插入图片描述
在这里插入图片描述
启动!!!

结果

在这里插入图片描述
这里只出了一个sql,完美

结果树里边的结果也是对的
在这里插入图片描述

总结

总体,我们就从问题复现 + 问题实现了
那我们就来谈谈这里的互斥锁,当然了互斥锁,是能解决问题的,但是性能其实还是有点影响的,但是一致性倒是保证了,接下来我们的另外一个解决办法,逻辑过期,就是牺牲了一致性,换来了性能!

逻辑过期实战

这里的实战也是比较复杂,希望你能认真阅读下去,并实现

问题复现

我们这里的问题复现,就不再赘述了,我写在了互斥锁实战中的问题复现中了,希望你能自己复现出来了!

解决

为了解决这里的问题,既然我们要写逻辑过期的代码,就得考虑如何做逻辑过期
这里有个地方需要注意,我们不能直接在实体类上写新的字段,这样写的代码有侵入性,不太优雅,我们得自己写一个类来实现这里的逻辑过期字段,就是如下

@Data
@Builder
public class RedisData {private LocalDateTime expireTime;private Object data;
}

这里就一目了然了,我们要把data封装进来就行了

在真正写代码之前,我们还是来捋一下流程,做到心中有数
在这里插入图片描述
我们来看这里的流程

  1. redis查缓存**(这里我们为了测试,就有缓存,并且是已经逻辑过期的缓存)**
  2. 判断缓存是否命中,没有命中,就返回空
  3. 缓存命中,就判断缓存是否过期
  4. 如果未过期,返回信息
  5. 如果已经过期,获取锁
  6. 获取锁失败,就返回信息(这里还是旧的信息)
  7. 获取锁成功,开一个独立的线程,去处理逻辑过期,还是返回旧的信息
  8. 最后结束

所以我们整体的流程看下来,我们可以总结一个逻辑,
只有缓存命中了,并且逻辑过期了,而且获取锁成功了,才要去开一个独立的线程处理这里的逻辑过期的事务
你好好斟酌我这里的逻辑,其他的情况都是返回旧的数据,所以说,为什么逻辑过期会有一致性问题,关键就在此处!,这就是奥妙!

准备

除了要封装逻辑过期类之外,我们还要先设置一个缓存数据到redis中,这里存redis代码如下

public void saveShop2Redis(Long id,Long expireTime) {Shop shop = getById(id);RedisData data = RedisData.builder().data(shop).expireTime(LocalDateTime.now().plusSeconds(expireTime)).build();redisCache.setCacheObject(RedisConstants.CACHE_SHOP_KEY + id,JSON.toJSONString(data));
}

我们在测试类中,先装载一个缓存先,以便后面测试用


@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Testpublic void test() {shopService.saveShop2Redis( 1L,30L);}
}

核心代码

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 解决缓存击穿 --> 逻辑过期* @param id* @return*/public Shop queryWithLogicExpire(Long id) {//查redisString shopKey = RedisConstants.CACHE_SHOP_KEY + id;String shopJSON = redisCache.getCacheObject(shopKey);//缓存没有,返回空,因为是热点数据Shop shop = null;//这里的未命中,包括了null和空串if(StrUtil.isBlank(shopJSON)) {return null;}//先查缓存是否逻辑过期RedisData redisData = JSONUtil.toBean(shopJSON, RedisData.class);shop = JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class);//如果过期了,就去获取锁if(LocalDateTime.now().isAfter(redisData.getExpireTime())) {boolean isLock = redisCache.tryLock(RedisConstants.LOCK_SHOP_KEY, "1");//如果获取成功了,就去开启一个独立的线程if(isLock) {//开启一个独立线程,去解决问题CACHE_REBUILD_EXECUTOR.submit(() -> {try {this.saveShop2Redis(id,30L);} catch (Exception e) {throw new RuntimeException();} finally {//释放锁redisCache.unlock(RedisConstants.LOCK_SHOP_KEY);}});}}//最后都是返回旧数据,牺牲一致性return shop;}public void saveShop2Redis(Long id,Long expireTime) {Shop shop = getById(id);RedisData data = RedisData.builder().data(shop).expireTime(LocalDateTime.now().plusSeconds(expireTime)).build();redisCache.setCacheObject(RedisConstants.CACHE_SHOP_KEY + id,JSON.toJSONString(data));}

我这里也来说明一下这里的代码,按照我上面分析的逻辑,只有说缓存命中 + 缓存过期 + 获取锁成功 才要去开启一个线程解决问题,在代码上也是体现了,就是这里的处理的逻辑我还得说一下
这里就是重新更新这里的key的逻辑过期时间

测试

这里的测试,有几个需要注意,我们得知道会出现的结果
首先,我们得知道,这里一定会有一致性问题的,所以我们得注意一致性问题,为了特别看到这里的一致性问题,我们得在测试前,更改数据库中的信息,以产生区别
其次,redis中的数据必须先装载上去,并且是已经逻辑过期的,你自己想想也知道,如果不是的化,那么就没有测试的必要了

redis中的数据
在这里插入图片描述

我们得修改数据库中的数据
在这里插入图片描述

jmeter的修改,为了看效果,就不要那么多线程数改成200
在这里插入图片描述

idea控制台清空
在这里插入图片描述
在这里插入图片描述
启动!!!

结果
先看,idea控制台

在这里插入图片描述
只有一个sql,完美

在查看这里的jmeter,看他的结果树

有些事旧的数据

在这里插入图片描述

后面已经都是新的数据
在这里插入图片描述

查看redis
在这里插入图片描述
这里也是正确的,所以没有问题!!!

总结

那么整体就ok了,这个逻辑过期也是牺牲了一致性的!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/249655.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elementUI实现selecttree自定义下拉框树形组件支持多选和搜索

elementUI实现selecttree自定义下拉框树形组件支持多选和搜索 效果图定义子组件父组件应用 效果图 定义子组件 主要结合el-select和el-tree两个组件改造的。 <template><div class"selectTree"><el-select filterable :filter-method"filterMe…

有趣的css - 动态的毛玻璃背景

页面效果 此效果主要使用 backdrop-filter 属性&#xff0c;以及配合 animation 属性来实现毛玻璃模糊和一些动效。 此效果可适用于登录窗口&#xff0c;网站背景或者一些卡片列表中&#xff0c;使网页更具科技感和空间感。 核心代码部分&#xff0c;简要说明了写法思路&#x…

RK3588开发板Ubuntu与开发板使用U盘互传

1 将 U 盘(U 盘的格式必须为 FAT32 格式&#xff0c;大小在 32G 以下)插到开发板的 usb 接口&#xff0c;串口打印信息如下所示&#xff0c;U 盘的设备节点是/dev/sdb4。U 盘的设备节点不是固定的&#xff0c;根据实际情况来查看设备节点。 2 输入以下命令挂载 U 盘&#xff0c…

计算机网络_1.4 计算机网络的定义和分类

1.4 计算机网络的定义和分类 一、计算机网络的定义&#xff08;无唯一定义&#xff09;二、计算机网络的分类&#xff08;从不同角度分类&#xff09;1、交换方式2、使用者3、传输介质4、覆盖范围5、拓扑结构 笔记来源&#xff1a; B站 《深入浅出计算机网络》课程 一、计算机…

Flask 入门2:路由

1. 前言 在上一节中&#xff0c;我们使用到了静态路由&#xff0c;即一个路由规则对应一个 URL。而在实际应用中&#xff0c;更多使用的则是动态路由&#xff0c;它的 URL是可变的。 2. 定义一个很常见的路由地址 app.route(/user/<username>) def user(username):ret…

安全防御第五次作业

拓扑图及要求如下&#xff1a; 实验注意点&#xff1a; 先配置双机热备&#xff0c;再来配置安全策略和NAT两台双机热备的防火墙的接口号必须一致双机热备时&#xff0c;请确保vrrp配置的虚拟IP与下面的ip在同一网段如果其中一台防火墙有过配置&#xff0c;最好清空或重启&…

基于 Echarts 的 Python 图表库:Pyecahrts交互式的日历图和3D柱状图

文章目录 概述一、日历图和柱状图介绍1. 日历图基本概述2. 日历图使用场景3. 柱状图基本概述4. 柱状图使用场景 二、代码实例1. Pyecharts绘制日历图2. Pyecharts绘制2D柱状图3. Pyecharts绘制3D柱状图 总结 概述 本文将引领读者深入了解数据可视化领域中的两个强大工具&#…

因子图、边缘化与消元算法的抽丝剥茧 —— Notes for “Factor Graphs for Robot Perception“

Title: 因子图、边缘化与消元算法的抽丝剥茧 —— Notes for “Factor Graphs for Robot Perception” 文章目录 I. 前言II. 因子图的基本概念1. 因子图的定义2. SLAM 中的因子图A. 因子图的图示B. 因子图的因式C. 因子图的二分图形式 III. 边缘化与消元运算的基本原理1. 边缘化…

python基础——池

池的介绍&#xff1a; 提前创建进程池&#xff0c;防止创建的进程数量过多导致系统性能受到影响&#xff0c;在系统执行任务时&#xff0c;系统会使用池中已经创建进程/线程&#xff0c;从而防止资源的浪费&#xff0c;创建的进程/线程可以让多个进程使用&#xff0c;从而降低…

Hadoop3.x基础(3)- MapReduce

来源: B站尚硅谷 目录 MapReduce概述MapReduce定义MapReduce优缺点优点缺点 MapReduce核心思想MapReduce进程常用数据序列化类型MapReduce编程规范WordCount案例实操本地测试提交到集群测试 Hadoop序列化序列化概述自定义bean对象实现序列化接口&#xff08;Writable&#xff…

安全基础~通用漏洞3

文章目录 知识补充文件上传&#xff08;1&#xff09;ctfshow 文件上传靶场练习150-161 文件上传&#xff08;2&#xff09;ctfshow 文件上传靶场练习162-170 文件上传总结文件包含 知识补充 url编码&#xff1a;0a 换行&#xff1b;20空格&#xff1b;3c左尖括号&#xff1b;…

研发日记,Matlab/Simulink避坑指南(八)——else if分支结构Bug

文章目录 前言 背景介绍 问题描述 分析排查 解决方案 总结归纳 前言 见《研发日记&#xff0c;Matlab/Simulink避坑指南(三)——向上取整Bug》 见《研发日记&#xff0c;Matlab/Simulink避坑指南(四)——transpose()转置函数Bug》 见《研发日记&#xff0c;Matlab/Simuli…

IP 层转发分组的过程

目录 IP 层转发分组的过程 1.1 基于终点的转发 1.2 最长前缀匹配 转发表中的 2 种特殊的路由 主机路由 (host route) 默认路由 (default route) 路由器分组转发算法 1.3 使用二叉线索查找转发表 IP 层转发分组的过程 1.1 基于终点的转发 分组在互联网中是逐跳转发的。…

VMware vCenter告警:vSphere UI运行状况警报

vSphere UI运行状况警报 不会详细显示告警的具体内容&#xff0c;需要我们自己进一步确认告警原因。 vSphere UI运行状况警报是一种监控工具&#xff0c;用于检测vSphere环境中的潜在问题。当警报触发时&#xff0c;通常表示系统遇到了影响性能或可用性的问题。解决vSphere UI…

【LVGL源码移植】

LVGL源码移植 ■ LVGL源码移植一&#xff1a;下载LVGL源码二&#xff1a;修改LVGL文件夹1: 将这5个文件&#xff0c;复制到一个新的文件夹2: 简化文件&#xff0c;减少内存消耗&#xff08;去除不必要的文件&#xff09;3: 为了规范化&#xff0c;我们将下列文件进行重命名 三&…

Apache POI 处理excel文件 记录用法

Apache POI 写excel public static void write() throws IOException {//再内存中创建了一个Excel文件XSSFWorkbook excel new XSSFWorkbook();//创建一个sheet页XSSFSheet sheet excel.createSheet("info");//这里创建行对象,这里的rownum 是从0开始的,类似于数…

大数据开发之离线数仓项目(用户行为采集平台)(可面试使用)

第 1 章&#xff1a;数据仓库概念 数据仓库&#xff0c;是为企业指定决策&#xff0c;提供数据支持的&#xff0c;可以帮助企业&#xff0c;改进业务流程、提高产品质量等。 数据仓库的输入数据通常包括&#xff1a;业务数据、用户行为数据和爬虫数据等。 业务数据&#xff1a…

维护管理Harbor,docker容器的重启策略

维护管理Harbor 通过HarborWeb创建项目 在 Harbor 仓库中&#xff0c;任何镜像在被 push 到 regsitry 之前都必须有一个自己所属的项目。 单击“项目”&#xff0c;填写项目名称&#xff0c;项目级别若设置为"私有"&#xff0c;则不勾选。如果设置为公共仓库&#…

【新书推荐】4.3节 键盘扫描码

本节内容&#xff1a;键盘扫描码。 ■键盘扫描码&#xff1a;8086计算机的键盘上的按键分为字符键、功能键和控制键。每一个按键都对应一个键盘扫描码。当按下按键时的扫描码称为通码&#xff0c;松开按键时的扫描码称为断码。如果按下的是字符键&#xff0c;则将其对应的一个…

假期刷题打卡--Day20

1、MT1173魔数 一个数字&#xff0c;把他乘以二&#xff0c;会得到一个新的数字&#xff0c;如果这个新数字依然由原数中那些数字组成&#xff0c;就称原数为一个魔数。输入正整数N&#xff0c;检查它是否是一个魔数&#xff0c;输出YES或者NO。 格式 输入格式&#xff1a; …