【Redis】Redis缓存击穿
1. 概述
缓存击穿:缓存击穿问题也叫热点key问题,一个高并发的key或重建缓存耗时长(复杂)的key失效了,此时大量的请求给数据库造成巨大的压力。如下图,线程1还在构建缓存时,线程2,3,4也来查询缓存,未命中目标到达数据库中查询数据并重建缓存。

2. 解决方案
针对缓存击穿,有两种解决方案。分别是互斥锁和逻辑过期。
2.1 互斥锁
思想:在众多的线程中,只有一个线程可以获得锁,获得锁的线程才能够重建缓存,再释放锁。没有获得锁的线程让其休眠一段时间后再次查询缓存,如果命中目标就返回数据了,如果还是没有命中目标,就再次尝试获得锁,如果获得锁就可以重建缓存,否则再休眠一段时间去缓存中查询,查看是否能命中目标,一直这样循环直到获得目标数据。
获得锁:这个锁不是我们常用的lock, synchronized锁,这两种锁拿到了就会执行,没拿到就等待。但我们这里的锁需要自定义拿到锁和未拿到锁需要干什么。这学习redis基本语法的时候,redis中有个命令和上诉的功能类似,那就是 setnx,如果key不存在就添加,返回结果是1,否则不添加,返回结果是0。
释放锁:释放锁直接将其删除就好了。del setnx
注意:为了避免锁没有被释放而造成死锁原因,最好设置一个有效期作为兜底,即便没有释放锁,有效期过后自动删除,就不会造成死锁了。

这种方式有一个缺点,如果重建缓存比较久,因为加了锁的原因,重建缓存的这段时间其它线程只能等待,性能不高。万一某个因素导致锁没有释放,会发生死锁的情况。
2.2 逻辑过期
逻辑过期,顾名思义,不是真正意义的过期,也可简单理解为永不过期。出现缓存击穿的问题也是key失效导致的,那么我们就不给缓存设置过期时间ttl了。不设置过期时间怎么维护这些缓存呢?总不能一直存在缓存中吧?当然不是了,我们可以在存储数据时再额外存入一个过期时间,后续我们只要维护这个额外的过期时间就好了。
但是换一个角度来看,这个过期时间是由开发人员添加的,redis并不会帮我们管理这些数据,也就是说,这些数据一旦存入redis中,在某种意义上这些数据是持久性的。
一般来说,这些热点key都是在商品做活动的时候用的多,我们会提前把这些高并发数据导入到缓存中,导入数据时就为它们添加逻辑过期时间,等活动结束后,将它们移除即可。另外,查询这些数据理论上来说是一定能命中的,如果没有命中,说明这个数据不是活动数据。所以说只需要判断这些数据是否逻辑过期即可。
那逻辑过期了,也就是说缓存中的是旧数据,需要重建缓存,为了解决线程安全问题,这里也是需要加锁的,但值得一提的是,获得锁的线程(线程1)并不会自己去重建缓存,而是重开一个线程(线程2),委托新线程(线程2)去重建缓存,线程1会先凑合使用旧数据。如果线程2在重建缓存期间,来了一个线程3,因为缓存过期了,必然会尝试获取锁,但锁已经被线程2获取了,所以线程3肯定是获取锁失败的,此时线程3知道了有人帮我们做缓存更新了,于是线程3也拿到过期的数据返回了。就在这时,线程2已经重建好了缓存,并把锁释放了。刚好来了一个线程4,在缓存中命中了目标数据,并返回了最新的数据。

2.3 总结
互斥锁就是在缓存重建的过程,让其他线程进行等待,从而确保数据一致性,但线程需要等待,如果锁没有释放,还会导致服务阻塞,甚至不可用的状态。
逻辑过期是保证在缓存重建期间服务依然可用,但不能保证数据一致性。

3. 实现
3.1 基于互斥锁解决缓存击穿

思想:利用redis的setnx方法来表示获取锁,该方法含义是如果redis中没有这个key,则插入成功,返回1。但是在spring中它帮我们转为了Boolean,因此在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。
// tryLock:尝试获取锁。锁就是redis中的一个key,所以key由使用者传给我们,我们就不在这写死了
private boolean tryLock(String key) {// 执行setnx,ctrl + p查看参数,可以发现它在存的时候是可以同时设置有效期的// 有效期的时长跟你的业务有关,一般正常你的业务执行时间是多少,你这个锁的有效期就比它长一点,长个10倍20倍(避免异常情况),例如这里就设置为10秒钟Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 这里不要直接将flag返回,因为直接返回它是会做拆箱的,在拆箱的过程中是有可能出现空指针的,因此这里建议大家使用一个工具类BooleanUtil,是hutool包中的,它可以帮你做一个判断(isTrue、isFalse方法),返回的是一个基本数据类型;或者它也可以直接帮你拆箱(isBollean方法)return BooleanUtil.isTrue(flag);
}// unlock:释放锁
private void unlock(String key) {// 之前分析过了,方法锁就是将锁删掉stringRedisTemplate.delete(key);
}
缓存击穿和缓存穿透的逻辑非常相似,可以在缓存穿透的基础上按照上面的流程图修改。
实现类
public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;// 1、从redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的值是否是空值if (shopJson != null) {//返回一个错误信息return null;}// 4.实现缓存重构,缓存重建业务比较复杂,不是一步两步就能搞定的// 4.1 获取互斥锁,是一个keyString lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判断否获取成功if(!isLock){// 4.3 失败,则休眠并重试// 休眠不要花费太长时间,这里可以先休眠50毫秒试一试,这个方法有异常,最后解决它Thread.sleep(50);// 重试就是递归即可return queryWithMutex(id);}// PS:获取锁成功应该再次检测redis缓存是否存在,做DoubleCheck。如果存在则无需重建缓存。但是这里先不检查了。// 4.4 成功,根据id查询数据库shop = getById(id);// 5.不存在,返回错误 // 这个是解决缓存穿透的if(shop == null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}// 6.写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);// 最后ctrl + T用try-catch-finally将代码包起来} catch (Exception e){// 这里异常我们就不去做处理了,因为sleep是打断的异常,直接往外抛即可throw new RuntimeException(e);}finally {// 7.释放互斥锁,因为抛异常的情况下,也是需要执行unlock的,因此需要放到unlockunlock(lockKey);}// 返回return shop;
}
根据上面的逻辑,为空直接返回null, 为了给用户一个良好的操作体验,查询数据时对返回结果做一个非空判断,给用户一个提示。
@Override
public Result queryById(Long id) {// 缓存穿透// Shop shop = queryWithPassThrough(id);// 互斥锁解决缓存击穿Shop shop = queryWithMutex(id);if (shop == null) {return Result.fail("店铺不存在!");}return Result.ok(shop);
}
3.2 基于逻辑过期解决缓存击穿
思想:当用户请求数据时,首先到redis缓存中查询,理论上讲这个是不会出现未命中的情况,因为现在key是不会过期的,因此我们可以认为,一旦这个key添加到了缓存里面,它应该会是永久存在的,除非活动结束,然后我们再删除。像这种热点key往往是一些参加活动的一些商品,我们会提前给它们加入缓存,在那个时候就会给它设置一下逻辑时间。但是在为了健壮性考虑,还是判断一下它有没有命中,真的未命中我们也不需要去做一些击穿、穿透这样的一些解决方案,我们直接给它返回空即可。
核心逻辑其实就是默认它命中了,在命中的情况下,我们需要判断的是它有没有过期,也就是它的逻辑过期时间,这个结果有两种:过期和不过期。如果没有过期,则直接返回redis中的数据,如果过期,那就说明它需要重新加载,去做缓存处理。但是不是任何线程都可以重建,因此这里需要有一个争抢,即它需要先尝试去获取互斥锁,然后判断获取是否成功,如果获取失败,说明在这之前有线程去获取数据库数据,那这个更新我们就不用管了,直接返回旧的即可。而获取锁成功的线程,就需要执行缓存重建,但是也不是自己去执行,而是开启一个独立的线程,由这个线程去执行缓存重建,它自己也是返回旧的数据先用着。
1. 设置逻辑过期时间
由于这个字段是我们为了解决缓存击穿才出现的,所以这个字段在实体类中必然是不存在的,有以下3中方式添加字段。
方式一:在实体类中添加字段,修改了原有代码,具有代码侵入性。(不推荐)
方式二:另外创建一个实体类存放逻辑过期字段,然后在实体类中继承新创建的类,也修改了原有代码,具有代码侵入性。(不推荐)
方式三:在 RedisData 中添加一个Object属性,也就是 RedisData 它自己带有过期时间,并且它里面带有数据,这个数据就是你想存进redis的数据,例如Shop、或者其他的数据,因此它是一个万能的存储对象。这种方案就完全不用对原来的实体类做任何修改
package com.hmdp.utils;@Data
public class RedisData {// 设置的逻辑过期时间private LocalDateTime expireTime;private Object data;
}
2. 缓存预热
这种热点数据,是需要提前将缓存导入进去的,实际开发中可能会有一个后台管理系统,可以把某一些热点提前在后台添加到缓存中,但由于我们现在没有一个后台管理的系统,因此基于单元测试方式来把数据加入到缓存中,充当是提前做一个缓存的预热。
下面这个方法将查询的数据写入到了缓存中,并为其封装了逻辑过期时间
// saveShop2Redis:将shop添加到redis中
public void saveShop2Redis(Long id, Long expireSeconds) {// 1.查询店铺数据Shop shop = getById(id);// 2.封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);// 过期时间由参数传进来redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3.写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
这里直接调用上面封装好的代码,模拟热点数据写入缓存中
@Test
void testSaveShop() {shopService.saveShop2Redis(1L, 10L);
}
3. 处理缓存击穿实现代码
3.1 设置一个常量类存放key, 和锁的过期时间
public static final String LOCK_SHOP_KEY = "lock:shop:"; // 店铺获取的锁(key)的前缀
public static final Long LOCK_SHOP_TTL = 10L; // 锁的过期时间
3.2 缓存穿透核心代码块
@Override
public Result queryById(Long id) {// 缓存穿透// Shop shop = queryWithPassThrough(id);// 互斥锁解决缓存击穿// Shop shop = queryWithMutex(id);// 逻辑过期解决缓存击穿Shop shop = queryWithLogicalExpire(id);if (shop == null) {return Result.fail("店铺不存在!");}return Result.ok(shop);
}private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否命中if (StrUtil.isBlank(json)) {// 3.未命中,直接返回nullreturn null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);// redisData.getData()返回的是Object类型,因为RedisData中的data类型是Object,所以使用JSON工具在做反序列化的时候,它并不知道你的类型是不是店铺Shop。此时redisData.getData()的返回值的本质其实是JSONObject,因此这里可以直接强转JSONObject data = (JSONObject) redisData.getData();// 当拿到JSONObject类型后,依旧使用JSON工具类,toBean除了可以接收JSON字符串以外,还可以接收JSONObject,然后告诉它我的实际类型是店铺,此时它就能返回给你一个店铺结果了Shop shop = JSONUtil.toBean(data, Shop.class);// 当然上面两步有点多余,完全可以放一步,但这里为了方便理解,依旧分为两步// Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期:过期时间是不是在当前时间之后?if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return shop;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 获取锁成功应该再次检测redis缓冲是否过期,做DoubleCheck。如果存在则无需重建缓存。// 6.3 成功,开启独立线程实现缓存重建。建议:使用线程池,不要自己去写一个线程,那一定话性能不太好,经常的创建和销毁。// 提交任务,这个任务我们可以写成一个Lambda表达式的形式CACHE_REBUILD_EXECUTOR.submit(()->{try {// 重建缓存,直接调用之前封装好的方法即可。// 这里过期时间准确来讲应该设置为30分钟,但是我们为了等一会测试,就先设置成20秒,我们期待的是缓存到底了,然后看看它会不会触发缓存重建的线程安全问题,因此设置短一点,方便我们观察效果this.saveShop2Redis(id, 20L);} catch (Exception e){throw new RuntimeException(e);} finally {// 重建缓存一定要释放锁,并且释放锁的动作最好写到finally中unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}
为了模拟重建缓存有延迟,这里休眠200毫秒。休眠时间越长,越容易引发线程安全问题。

相关文章:
【Redis】Redis缓存击穿
1. 概述 缓存击穿:缓存击穿问题也叫热点key问题,一个高并发的key或重建缓存耗时长(复杂)的key失效了,此时大量的请求给数据库造成巨大的压力。如下图,线程1还在构建缓存时,线程2,3&…...
厦门凯酷全科技有限公司深耕抖音电商运营
在数字经济飞速发展的今天,抖音电商平台以其独特的社交属性和庞大的用户基础,迅速成为众多品牌和商家的新战场。在这个充满机遇与挑战的市场中,厦门凯酷全科技有限公司凭借其专业的服务、创新的理念和卓越的执行力,成为了抖音电商…...
六西格玛DMAIC在企业得项目管理中有什么作用
六西格玛(Six Sigma)是一种以数据为基础的管理方法,旨在通过减少缺陷和变异来提高过程质量和效率。DMAIC 是六西格玛中一种常用的改进方法论,适用于现有过程的改进。DMAIC 代表五个阶段:定义(Define&#x…...
vscode借助插件调试OpenFoam的正确的.vscode配置文件
正确的备份文件位置: /home/jie/桌面/理解openfoam/正确的调试爆轰单进程案例/mydebugblastFoam 调试爆轰案例流体 并且工作区和用户区都是openfoam-7版本 问题:F5以debug模式启动后不停在断点 解决方法: 这里备份一下.vsode正确的配置&…...
SpringBoot整合JWT(JSON Web Token)生成token与验证
目录 JWT 什么是JWT JWT使用流程 确定要传递的信息: 生成JWT: JWT传输: 客户端保存JWT: 客户端发送JWT: 服务器验证JWT: 服务器响应: Token的使用示例: 工具类 R结果集 返回一个生成的token 创建拦截器 JWT 什么是JWT JWT(JSON Web Token)是是目前最…...
把帕拉丁需要的.rom文件转成.bin
# 输入文件名 input_file_name = fw_payload.bin.rom # 输出文件名 output_file_name = fw_payload.bin.rom2 # 打开输出文件,准备写入翻转后的十六进制字符串 with open(output_file_name, w) as output_file: # 打开输入文件读取十六进制字符串 with open(input_f…...
Nginx 缓存那些事儿:原理、配置和最佳实践
Nginx 缓存那些事儿:原理、配置和最佳实践 在当今的互联网世界,网站的访问量和数据处理量不断攀升,如何确保用户能够快速、稳定地访问我们的网站,已经成为每个运维工程师面临的挑战。幸运的是,Nginx 作为一款高性能的…...
vue发展史
Vue.js发展史 Vue.js是一个渐进式JavaScript框架,自发布以来受到了广泛的关注和喜爱。以下是Vue.js的发展史: 1. 起源(2013年) Vue.js的创始人尤雨溪(Evan You)在2013年开始构思这个项目。当时࿰…...
基于Java和Vue开发的校园跑腿软件校园跑腿小程序系统源码
市场前景 学生需求多样化: 随着校园生活节奏的加快和学生需求的多样化,跑腿服务逐渐成为一种新兴的商业模式。学生群体对于便捷、高效的日常服务需求不断增加,如外卖送餐、快递代取、文件传递等。市场规模持续增长: 大学校园作为…...
MySQL(五)--- 事务
1、CURD操作不加控制时,可能会出现什么问题 即:类似于线程安全问题,可能会导致数据不一致问题。 因为,MySQL内部本身就是多线程服务。 1.1、CURD满足什么属性时,才能避免上述问题 1、买票的过程得是原子的吧。 2、买票互相应该不能影响吧。 3、买完票应该要永久有效吧。…...
llm chat场景下的数据同步
背景 正常的chat/im通常是有单点登录或者利用类似广播的机制做多设备间内容同步的。而且由于长连接的存在,数据同步(想起来)相对简单。而llm的chat在缺失这两个机制的情况下,没见到特别好的做到了数据同步的产品。 llm chat主要两…...
机器学习经典算法
机器学习经典算法学习和分享。 k近邻算法 线性回归 梯度下降法 PCA主成分分析法 多项式回归 逻辑回归 支撑向量机SVM 决策树 随机森林 评价分类指标...
Scala中的泛型
类型参数 ---- 泛型(数据类型是变化的) (1) 可以有多个 (2) 名称合法就行,没有固定的,一般用T(Type) 在Scala中,用[]表示。在Java中用<>表示 1. 与数据类型的区别 List是数据类型,表示一个列表。[Int]表示泛型,它…...
数据分析特征标准化方法及其Python实现
数据分析特征标准化方法及其Python实现 1、概述 在数据分析中,对特征进行标准化主要是: 1、消除量纲影响 不同特征可能具有不同的量纲和数量级。 例如,一个特征可能是以米为单位的长度,而另一个特征可能是以秒为单位的时间。直接使用这些具有不同量纲的原始数据进行分析…...
UnityShaderLab 实现程序化形状(一)
1.实现一个长宽可变的矩形: 代码: fixed4 frag (v2f i) : SV_Target{return saturate(length(saturate(abs(i.uv - 0.5)-0.13)))/0.03;} 2.实现一个半径可变的圆形: 代码: fixed4 frag (v2f i) : SV_Target{return (distance(a…...
前端数据安全防护(控制台)
目录 前言 禁用右键菜单 禁用快捷键 监控控制台 完整逻辑 前言 前端的数据在浏览器中一直处于一个裸奔的状态,只要是稍微懂一点计算机的人,都可以在浏览器的控制台中拿到前端页面的所有数据,包括和后端的交互数据。为了…...
自己玩虚拟机:vagrant,virtual box,centos
vagrant 访问Vagrant官网 https://www.vagrantup.com/ 点击Download Windows,MacOS,Linux等 选择对应的版本 AMD64 (x86_64) I686 (x86) 傻瓜式安装 命令行输入vagrant,测试是否安装成功 vagrant -v 可以查看当前版本 virtual box 访…...
Frida框架HOOK RegisterNatives函数
使用Frida框架HOOK RegisterNatives函数,获取动态注册的函数地址、名称、签名、class名称、所属的so文件名称、so文件加载基址、函数在so文件中的地址。 废话不多说,上代码: 运行命令:frida -U -f in.****** -l RegisterNatives…...
[创业之路-189]:《华为战略管理法-DSTE实战体系》-2- 生存与发展的双重旋律:短期与长期、战术与战略的交响乐章
目录 生存与发展的双重旋律:短期与长期、战术与战略的交响乐章 一、生存:短期视角下的战术布局 二、发展:长期视角下的战略规划 三、短期与长期、战术与战略的融合与平衡 四、结语:在生存与发展的交响曲中奏响辉煌 生存与发展…...
TDengine 部署
TDengine是一款开源高性能的时序数据库,其部署过程可以根据不同的环境和需求进行灵活配置。以下将详细介绍TDengine的部署步骤,包括单节点部署和集群部署。 一、单节点部署 下载安装包: 访问TDengine的官方网站或GitHub仓库,下载…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
