Redis:原理速成+项目实战——Redis实战5(互斥锁、逻辑过期解决缓存击穿问题)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战4(解决Redis缓存穿透、雪崩、击穿)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

上一篇文章讲解了缓存击穿问题,以及解决缓存击穿问题的2种解决思路,即互斥锁与逻辑过期,这里将分别用这两种方式解决缓存击穿问题。

互斥锁、逻辑过期解决缓存击穿问题

  • 互斥锁解决缓存击穿
    • 获取锁与释放锁方法封装
    • 业务逻辑修改
    • 测试
  • 逻辑过期解决缓存击穿
    • 代码实现
    • 测试

互斥锁解决缓存击穿

根据上次讲解的互斥锁解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
这里有比较关键的点,这里的锁并不是我们平常用的锁,平常的锁如果没有获取到的话就一直等待,而这边我们需要自定义逻辑。
我们该如何自定义获取锁跟释放锁的逻辑?我们可以利用Redis的一些数据结构的特性。
我们可以联想到之前学习Redis数据结构的时候,用到的setnx,当且仅当key不存在的时候,setnx才会执行set操作:
在这里插入图片描述
可以看到,当我们的key已经是存在的情况下,我们用setnx指令无法对其value进行修改。所以如果有无数线程进行setnx操作的时候,只有第一个进行操作的线程可以写入value,其他线程都会失败。
所以,这就满足了我们所说的互斥锁的实现方式,其实这也是分布式锁的一个基本原理(当然真正的分布式锁还是比较复杂的)。
而释放锁只需要执行del操作即可,这样其他线程就可以获取到这个锁了:
在这里插入图片描述
当然了,最好别把锁永远加在那,因为如果加了锁的线程,他的工作没办法完成,他就永远没办法释放锁,因此我们在执行setnx操作的时候需要设置一下有效期。

获取锁与释放锁方法封装

进行业务逻辑编写之前,我们可以先把获取锁与释放锁的方法给自定义一下:

private boolean tryLock(String key){//尝试获取锁//opsForValue里面没有真正的setnx,而是setIfAbsent,表示如果不存在就执行set//值就随便设定一下,重点是要获取到锁,但是设定了TTL为10sBoolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);/*** 如果是直接返回flag,可能会有拆箱操作,造成空指针,需要用BooleanUtil工具类* 因为Boolean不是基本类型的boolean,是boolean的封装类*/return BooleanUtil.isTrue(flag);
}private void unlock(String key){//释放锁stringRedisTemplate.delete(key);
}

业务逻辑修改

我们可以先把之前会出现缓存穿透的代码给封装起来,封装成返回值为Shop类型的,防止代码丢失(之后章节的代码优化会解决缓存穿透问题):

public Shop queryWithPassThrough(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//判断命中的是否是nullif (shopJson != null){return null;}//不存在,根据id查询数据库Shop shop = getById(id);if (shop == null){//存一个null到Redis中//这种没用的信息,TTL没必要设置太长了,这里我设置成了2minstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//返回return shop;
}

同样的,我们也可以把互斥锁解决缓存击穿的代码给单独封装起来,注意,我们不管获得互斥锁的线程有没有正常执行,即便执行不成功,也要在最后进行unlock,因此我们要利用try…catch…finally:

public Shop queryWithMutex(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//判断命中的是否是nullif (shopJson != null){//返回错误信息return null;}//未命中,开始实现缓存重建//尝试获取互斥锁,锁的key跟缓存的key不一样String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);//判断是否获取成功if (!isLock){//失败则休眠并重试(递归)Thread.sleep(50);//单位:ms//如果担心递归造成爆栈,可以用循环,一样的return queryWithMutex(id);}//成功就直接开始重建(查询数据库并处理后保存到Redis)shop = getById(id);//由于在这里我们的数据库在本地,重建很快,所以添加休眠时间来模拟数据库重建过程的耗时长,方便测试Thread.sleep(200);//不存在,返回错误if (shop == null){//存一个null到Redis中//这种没用的信息,TTL没必要设置太长了,这里我设置成了2minstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {//释放互斥锁unlock(lockKey);}//返回return shop;
}

queryById方法调用这个方法即可:

    @Overridepublic Result queryById(Long id) {//缓存穿透的代码调用//Shop shop = queryWithPassThrough(id);//互斥锁解决缓存击穿Shop shop = queryWithMutex(id);if (shop == null) {return Result.fail("店铺不存在");}//返回return Result.ok(shop);}

测试

我们先把缓存清空,然后运行代码,再进行测试,我们可以使用一个很好用的测试工具jmeter,下载可以看下面这个博主的文章:
jmeter下载教程
官网下载的实在是太慢了,建议大家直接去这个博主的网盘里面下载。
我们可以这样设定,我们设定我们的并发量为1000,即有1000个线程同时访问:
在这里插入图片描述
并配置好我们的http请求:
在这里插入图片描述
运行完以后打开查看结果树:
在这里插入图片描述
数据都查到了。
打开汇总报告可以看到我们的吞吐量接近200:
在这里插入图片描述
如此大的数据量打下去,但是我们的日志显示我们的数据库只执行了一次的查询语句:
在这里插入图片描述
说明我们已经使用互斥锁成功避免了缓存击穿。

逻辑过期解决缓存击穿

代码实现

根据上次讲解的逻辑过期解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
其实通常情况,因为是热点key,一般都是会出现在Redis里面的,且因为我们没有设置TTL,所以热点key是一定会一直存在的,但为了严谨起见,还是在判定缓存未命中的时候返回空。
我们主要解析一下主要的流程:

1、我们判断一下缓存是否逻辑过期了,如果没有过期,我们直接返回信息到客户端即可
2、如果缓存逻辑过期了,这个线程就尝试获取互斥锁,如果获取成功,说明它是第一个访问Redis的这个过期key的线程,那么这个线程要做2件事:
(1)返回这个旧数据给客户,虽然数据是旧的,但是这是一种暂时的牺牲
(2)开辟新的线程来进行缓存数据的重建,重建完毕就释放这个互斥锁
3、除了第2种情况说的这个线程,其他线程在知道自己访问的数据过期之后,获取互斥锁都会失败,那么这时候只需要直接返还数据给客户就好了,可能是旧数据,也可能是新数据(第一个线程释放锁或者缓存数据重建成功了)

首先我们要对Shop类增加逻辑过期时间这样一个字段,一种方案是直接添加,这种会违背开闭原则,一种是可以新增加一个类,并且类中包含了逻辑过期时间expireTime,但是该怎么把这个属性添加到Shop里面呢?可以让Shop继承这个类,就可以获得这个类中的属性,但同样会修改Shop这个类的源代码,同样违背开闭原则,所以最好的方法就是用关联来代替继承:
在这里插入图片描述
如果不是很清楚的话可以先去学一下Java设计模式。

为了方便演示,我们首先要进行一个数据预热,我们需要有这个一个带着逻辑过期时间的店铺信息,因此这里就先用单元测试的方式,来让带着expireTime属性的数据存入Redis里面。

1、先编写一个类,用于将带着逻辑过期时间的Shop存入Redis里面:

    public void saveShop2Redis(Long id, Long expireSeconds){//查询店铺数据Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();//注入Shop对象redisData.setData(shop);//注入逻辑过期时间redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入Redis,不要设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

2、编写单元测试代码:

@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Testvoid testSaveShop(){shopService.saveShop2Redis(1L, 10L);}}

我们运行这个单元测试后,打开Redis客户端:
在这里插入图片描述
证明已经存储成功。

现在正式开始用逻辑过期思想去解决缓存击穿的问题,编写这个方法:

	private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public Shop queryWithLogicalExpire(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isBlank(shopJson)) {//未命中,直接返回return null;}//命中,先把json反序列化成对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);JSONObject data = (JSONObject) redisData.getData();//注意我们获取到了data信息以后,返回的会是一个JSONObject的格式Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//判断是否过期if (expireTime.isAfter(LocalDateTime.now())){//未过期,直接返回店铺信息return shop;}//已过期,缓存重建//获取互斥锁String lockKey = LOCK_SHOP_KEY + id;//LOCK_SHOP_KEY="lock:shop:"boolean isLock = tryLock(lockKey);//判断是否获取锁成功if (isLock){//成功获取锁,开启独立线程来实现缓存重建,用线程池来做CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);}finally {//释放锁unlock(lockKey);}});}//没有成功获取,直接返回过期商铺信息return shop;}

为了方便验证,之前的缓存重建函数我们给他增加一个模拟重建时间200ms:

    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {//查询店铺数据Shop shop = getById(id);Thread.sleep(200);//封装逻辑过期时间RedisData redisData = new RedisData();//注入Shop对象redisData.setData(shop);//注入逻辑过期时间redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入Redis,不要设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

之前存储在Redis的数据,其实早就已经逻辑过期了,只是因为我们没有设置TTL,它依旧存在于Redis里面,只是可能已经过期了。
queryById直接改成调用该方法,接着运行代码。

测试

1、我们直接将数据库中的餐厅名从101修改为102,使得数据库数据与缓存不一致:
在这里插入图片描述

2、打开jmeter进行测试,这里把线程数改小一点,不然不好观察:
在这里插入图片描述
运行后,观察结果树,可以发现我们一开始查到的就是旧数据,且这个过程会持续一段时间:
在这里插入图片描述
到后面,查询到的数据就是正常的数据了:
在这里插入图片描述
查看idea后台,可以发现我们只执行了一次重构,说明只有一个线程操作了数据库,其他数据库都被互斥锁拦住了:
在这里插入图片描述
这也证明了逻辑过期方法会造成短暂的数据不一致的情况。

综上,缓存击穿问题的两种解决思想,其demo都已经调通。

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

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

相关文章

Python(30):非对称加密算法RSA的使用(openssl生成RSA公私钥对)

Python(30):非对称加密算法RSA的使用(openssl生成RSA公私钥对) 1、openssl生成RSA公私钥对 1.1、生成RSA公私钥对命令 [rootloaclhost ~]# openssl OpenSSL> genrsa -out rsa_private_key.pem 1024 Generating RSA private key, 1024 bit long modulus .. ...…

LeetCode(209)长度最小的子数组⭐⭐

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。 示例: 输入:s 7, nums [2,3,1,2,4,3]输出:2…

AIGC无人直播系统技术

随着信息技术的快速发展和互联网的普及,直播行业迎来了蓬勃发展的机遇。然而,传统的直播方式存在一些局限性,如场地限制、设备携带不便等问题。为了解决这些问题,AIGC推出了一项创新性的无人直播系统技术。 AIGC无人直播系统技术…

如何寻找到相对完整的真正的游戏的源码 用来学习?

在游戏开发的学习之路上,理论与实践是并重的两个方面。对于许多热衷于游戏开发的学习者来说,能够接触到真实的、完整的游戏源码无疑是一个极好的学习机会。但问题来了:我们该如何寻找到这些珍贵的资源呢? 开源游戏项目 GitHub:地…

还不了解 Dockerfile 的同学不是好测试人

前言:近年来 Docker 非常火,想要玩好 Docker 的话 Dockerfile 是绕不开的,这就好比想要玩好 Linux 服务器绕不开 shell 道理是一样的。 今天我们就来聊一聊 Dockerfile 怎么写,那些指令到底是什么意思。 一、先来看一个简单的 Do…

RK3568上如何使用MPP进行硬解码

目录 前言正文一、FFmpeg 拉流处理二、RK3568 mpp硬解码1、简介2、普通mpp解码流程3、核心代码 END、总结的知识与问题1、一直出现jitter buffer full 这样的问题2、如何打印帧率?3、分析av_packet_alloc、av_init_packet、av_packet_unref、av_packet_free、av_fra…

机器人技能学习-robosuite-0-入门介绍

文章目录 前言模块介绍实战案例1:从 demo 中创建自己的 env案例2:更换属于自己的物体 前言 资料太少、资料太少、资料太少,重要的事说三边,想根据自己实际场景自定义下机器人,结果发现无路可走,鉴于缺少参…

软件测试概念及分类整理汇总

前言 测试小伙伴在谈论软件测试分类,五花八门的分类,眼花缭乱。因为将各个维度划分的内容都整到一块了,在加上各自不同的见解与补充,各种冲突...... Findyou我经过多年测试总结基本定为4类测试(最多5类,自动化或者兼容…

phpstudy面板Table ‘mysql.proc‘ doesn‘t exist解决办法

原因分析:误删了mysql数据库 解决办法如下: 1、停止服务 2、先把mysql文件夹下的data文件夹备份,因为data文件里存有数据库文件。然后再删除data文件。 3、cmd管理员命令进入到mysql中的bin目录下 ,执行mysqld --initialize-…

【Python机器学习】用于回归的决策树

用于回归的决策树与用于分类的决策树类似,在DecisionTreeRegressor中实现。DecisionTreeRegressor不能外推,也不能在训练数据范围之外的数据进行预测。 利用计算机内存历史及格的数据进行实验,数据展示: import pandas as pd im…

.mkp勒索病毒数据怎么处理|数据解密恢复

导言: 在数字时代,勒索病毒如[datastorecyberfear.com].mkp [hendersoncock.li].mkp [myersairmail.cc].mkp正成为企业和个人的噩梦。本文将介绍[datastorecyberfear.com].mkp [hendersoncock.li].mkp [myersairmail.cc].mkp勒索病毒的特点、如何恢复被…

LIN总线故障检测

关注菲益科公众号—>对话窗口发送 “CANoe ”或“INCA”,即可获得canoe入门到精通电子书和INCA软件安装包(不带授权码)下载地址。 LIN总线自诊断 对LIN数据总线系统进行自诊断时,需使用“LIN主控模块”的“地址码”。 自诊断数…

大功率TVS的基本介绍?|深圳比创达电子

在当今的电子世界里,电路保护变得越来越重要。那么什么是大功率TVS(Transient Voltage Suppressor)?它又是如何发挥作用的呢?TVS二极管,被设计用来保护敏感电子设备免受瞬态电压冲击,尤其是在功率较高的应用中尤为关键…

一文详解VScode 的远程开发

VS code登录服务器后进行编码和调试,VS code上的所有功能都可以使用,和在本地开发基本无区别。 一、配置免密远程登录 因为是要远程登录,那么需要通过使用ssh进行密钥对登录,这样每次登录服务器就可以不用输入密码了。 先来一句官…

常见测试技术都有哪些?

测试技术是用于评估系统或组件的方法,目的是发现它是否满足给定的要求。系统测试有助于识别缺口、错误,或与实际需求不同的任何类型的缺失需求。测试技术是测试团队根据给定的需求评估已开发软件所使用的最佳实践。这些技术可以确保产品或软件的整体质量…

TDuckX 新功能介绍:提交后抽奖!

欢迎来到 TDuckX,我们最近推出了令人兴奋的新功能——提交后抽奖!现在,您不仅可以收集用户的宝贵意见,还有机会为他们带来丰厚的奖励。让我们一起来了解这个令人期待的新功能吧! 主要功能亮点: 1. 奖品设…

adb 使用的出现 adb server is out of date. killing

我知道你们肯定看到这种播客 像这种解决问题的方法是暂时的 , 因为我们Android studio 中 , 你在查看后台的时候发现 你的Android studio 也占用了一端口 , 就是说你把 Android studio 杀掉了只要打开 Android studio 打开就还是 关闭 cmd adb 看到一个新的方法 , win 10 中…

Spring——Spring AOP1(代理模式Proxy)

代理(Proxy)模式 1.创建工程 2.代理(Proxy)模式介绍 作用:通过代理可以控制访问某个对象的方法,在调用这个方法前做前置处理,调用这个方法后做后置处理。(即: AOP的微观…

单片机原理及应用:中断服务函数

承接上文,今天我们来学习一下中断服务函数,对中断不了解的朋友可以回顾一下笔者之前的文章 中断系统结构与控制寄存器 中断服务函数是嵌入式系统中用于处理中断事件的函数,在原版的C语言中并不存在。当发生中断事件时,系统将会跳…

(vue)el-cascader级联选择器实现单/多选最后一级并回显

(vue)el-cascader实现多选最后一级并回显 <el-form-item label"选择算法模型&#xff1a;"><el-cascaderv-model"formInline.algorithmId":options"modelOptions":props"{ value: id, label: name, multiple: true, emitPath: fal…