缓存相关问题记录解决

缓存相关问题

在这里我不得不说明,我写的博客都是我自己用心写的,我自己用心记录的,我写的很详细,所以会有点冗长,所以如果你能看的下去的化,会有所收获,我不想写那种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.mzph.cn/news/660664.shtml

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

相关文章

TypeScript(十) Map对象、元组、联合类型、接口

1. Map对象 1.1. 简述 Map对象保存键值对&#xff0c;并且能够记住键的原始插入顺序。   任何值都可以作为一个键或一个值。 1.2. 创建 Map 使用Map类型和new 关键字来创建Map&#xff1a; 如&#xff1a; let myMap new Map([["key1", "value1"],[&…

探索未来发展方向:图片转换为Excel表格的智能化与自动化

随着科技的不断进步&#xff0c;人工智能技术已经在许多领域得到广泛应用。其中&#xff0c;将图片转换为Excel表格的智能化与自动化技术成为了备受关注的新兴领域。这一技术的发展&#xff0c;不仅可以极大地提高工作效率&#xff0c;还能为数据分析提供更为准确和便捷的方式。…

PRBS并行输出

PRBS&#xff08;Pseudo-Random Binary Sequences&#xff09;是通过LFSR和特征函数 伪随机数发生器产生的伪随机数序列&#xff0c;通常用于高速数字通信测试。 基本电路&#xff08;单比特输出&#xff09; prbs N表示用N比特lfsr尝试伪随机数序列&#xff0c;常用的有N7,9…

创建与删除数据库(四)

创建与删除数据库&#xff08;四&#xff09; 一、创建数据库 1.1 使用DDL语句创建数据库 CREATE DATABASE 数据库名 DEFAULT CHARACTER 示例&#xff1a; 创建一个test 的数据库&#xff0c;并查看该数据库&#xff0c;以及该数据库的编码。 创建数据库&#xff1a; cre…

4G路由器助力智慧农业数据采集与远程管理

随着科技日新月异的发展&#xff0c;智慧农业正逐渐改变着传统农业生产模式。4G路由器作为物联网技术的关键通信设备&#xff0c;在实现农业现场传感器数据实时采集与远程在线管理方面发挥着重要作用&#xff0c;以下智联物联分享4G路由器在智慧农业中的应用优势。 农业现场传感…

自动保存知乎上点赞的内容至本地

背景&#xff1a;知乎上常有非常精彩的回答/文章&#xff0c;必须要点赞收藏&#xff0c;日后回想起该回答/文章时翻看自己的动态和收藏夹却怎么也找不到&#xff0c;即使之前保存了链接网络不好也打不开了&#xff08;。所以我一般碰到好的回答/文章都会想办法保存它的离线版本…

社交买量:归因统计的核心要素与工具

在当今的社交App推广领域&#xff0c;广告买量已成为企业获取用户的重要手段。然而&#xff0c;如何准确衡量这些买量活动的成效&#xff0c;即用户从广告访问到安装后行为的完整转化路径&#xff0c;一直是运营人员关注的焦点。归因统计是一种评估营销效果的关键技术方案&…

python爬虫-多线程-数据库——WB用户

数据库database的包&#xff1a; Python操作Mysql数据库-CSDN博客 效果&#xff1a; 控制台输出&#xff1a; 数据库记录&#xff1a; 全部代码&#xff1a; import json import os import threading import tracebackimport requests import urllib.request from utils im…

论文阅读-一个用于云计算中自我优化的通用工作负载预测框架,

论文标题&#xff1a;A Self-Optimized Generic Workload Prediction Framework for Cloud Computing 概述 准确地预测未来的工作负载&#xff0c;如作业到达率和用户请求率&#xff0c;对于云计算中的资源管理和弹性非常关键。然而&#xff0c;设计一个通用的工作负载预测器…

【每日一题】6.LeetCode——轮转数组

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》|《数据结构与算法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢…

PVE报错处理:kvm [2205]: vcpu0 ignored RDMSR: 0x1b8

PVE使用过程中如果遇到&#xff1a;kvm [2205]: vcpu0 ignored RDMSR: 0x1b8 报错信息处理方法 vim /etc/modprobe.d/kvm.conf "options kvm ignore_msrsY"&#xff0c;这里在msrsY后面加一个空格&#xff0c;然后粘贴report_ignored_msrsN&#xff0c;使其变成 op…

为什么建议不要买入耳式的耳机?有没有不伤听力的蓝牙耳机

为什么建议不要买入耳式的耳机&#xff1f;因为长时间佩戴入耳式耳机可能会导致耳朵不适甚至疼痛&#xff0c;且存在听力损伤、耳膜损伤的风险&#xff0c;还可能诱发耳道发炎。那么有没有不伤听力的蓝牙耳机呢&#xff1f;当然是有的&#xff0c;我建议尝试一下骨传导蓝牙耳机…

简单实践 java spring cloud 负载均衡

1 概要 1.1 实现一个最简单的微服务。远程调用负载均衡&#xff0c;基本上完成了最核心的微服务框架。 远程调用&#xff1a;RestTemplate 注册中心&#xff1a;eureka 负载均衡&#xff1a;Ribbon 1.2 要点 1.2.1 依赖 1.2.1.1 主框架依赖 spring boot 依赖 <depe…

GPT-4 Vision根据应用程序截图生成博客和Readme 升级Streamlit八

GPT-4 Vision 系列: 翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式一翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式二翻译: GPT-4 Vision静态图表转换为动态数据可视化 升级Streamlit 三翻译: GPT-4 Vision从图像转换为完全可编辑的表格 升级St…

虚拟机(VMware)ubuntu16.04 直接连接网口设备 USRP 吊舱

编辑虚拟网络编辑器 点击之后 选择网卡之后&#xff0c;点击确定。 电脑配置 使用了&#xff1a;192.168.2.56 虚拟机内部配置 和PC的配置一致

leetcode刷题(剑指offer) 101.对称二叉树

101.对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; …

Elasticsearch性能调优

背景 项目上是用 ES 做数据库&#xff0c;存储的告警数据&#xff0c;量级在千万级别左右。测试在压测之后&#xff0c;系统频繁出现告警记录查询报错&#xff0c;系统不可用。基于此排查分析项目上 Elasticsearch 的使用是否合理。 版本及硬件 环境&#xff1a;10.xx.xxx.x…

vue前端html导出pdf

package.json中添加依赖 调用方&#xff1a; import htmlToPdf from ../../../utils/file/htmlToPdf.js// 下载方法&#xff0c;pdfDownloadDpi为onClickDownLoad() {htmlToPdf.getPdf(标题1, jsfgyzcpgxmShow, this.pdfDownloadDpi)}htmlToPdf.js // 页面导出为pdf格式 imp…

漏洞01-目录遍历漏洞/敏感信息泄露/URL重定向

目录遍历漏洞/敏感信息泄露/URL重定向 文章目录 目录遍历敏感信息泄露URL重定向 目录遍历 敏感信息泄露 于后台人员的疏忽或者不当的设计&#xff0c;导致不应该被前端用户看到的数据被轻易的访问到。 比如&#xff1a; ---通过访问url下的目录&#xff0c;可以直接列出目录下…

秋招面试—JS篇

2024 JavaScript面试题 1.new 操作符的工作原理 ①.创建一个新的空对象 ②.将这个对象的原型设置为函数的 prototype 对象 ③.让函数的this指向该对象&#xff0c;为函数添加属性和方法 ④.最后返回这个对象 2.什么是DOM&#xff0c;什么是BOM? DOM&#xff1a;文档对象…