黑马点评-07缓存击穿问题(热点key失效)及解决方案,互斥锁和设置逻辑过期时间
缓存击穿问题(热点key失效)
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且重建缓存业务较复杂的key
突然失效了,此时无数的请求访问会在瞬间打到数据库,带来巨大的冲击
- 一件秒杀中的商品的key突然失效了,由于大家都在疯狂抢购那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿
互斥锁
如果缓存中没有缓存对应的店铺信息时,所有的线程过来后需要先获取锁才能查询数据库中的店铺信息,保证只有一个线程访问数据库,避免数据库访问压力过大
- 优点: 实现简单且没有额外内存销毁(加一把锁), 当拿到线程锁的线程把缓存数据重建好后,其他线程再访问时从缓存中查询的数据和数据库中的数据就是一致的
- 缺点: 当拿到线程锁的线程在操作数据库的时候,其他线程只能等待,将查询的性能从并行变成了串行(tryLock方法+double check可以解决),但是还有死锁的风险
setnx实现互斥锁
根据店铺Id查询商铺信息
,增加了获取互斥锁的环节,即缓存未命中时只有获取锁成功的线程才能查询数据库,保证只有一个线程去数据库执行查询语句
,防止缓存击穿
利用redis提供的setnx key(锁Id) value
命令判断是否有线程成功插入key(锁), del key
表示释放锁
返回值 | 描述 |
---|---|
0 | 表示线程插入key失败,即线程获取锁失败 |
1 | 表示线程插入key成功即线程获取锁成功 |
在StringRedisTemplate
中对应setnx指令的方法是setIfAbsent()
,返回true表示插入成功,fasle表示插入失败
// 每一个店铺都有自己的锁,根据锁的Id(锁前缀+店铺ID)尝试获取锁(本质是插入key)
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 我们这里使用了BooleanUtil工具类将Boolean类型的变量转化为boolean,避免在拆箱过程中返回nullreturn BooleanUtil.isTrue(flag);
}// 释放锁(本质是删除key)
private void unlock(String key) {stringRedisTemplate.delete(key);
}
单独实现负责解决缓存击穿
问题的方法queryWithMutex
,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById
中做统一判断返回结果类
- 获取锁成功,应该再次检测redis缓存是否存在,因为此时可能其他线程重建完缓存刚释放完锁后,做双重检查,如果存在则无需重建缓存
@Override
public Result queryById(Long id) { // 使用互斥锁解决缓存击穿Shop shop = queryWithMutex(id);// 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串if (shop == null) {return Result.fail("店铺不存在!!");}// shop不等于null,把查询到的商户信息返回给前端return Result.ok(shop);
}
@Override
public Shop queryWithMutex(Long id) {//1.先从Redis中查询对应的店铺缓存信息,这里的常量值是固定的店铺前缀+查询店铺的IdString shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.如果在Redis中查询到了店铺信息,并且店铺的信息不是空字符串则转为Shop类型直接返回,""和null以及"/t/n(换行)"都会判定为空即返回falseif (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//3.如果命中的是空字符串即我们缓存的空数据,返回nullif (shopJson != null) {return null;}// 4.没有命中则尝试根据锁的Id(锁前缀+店铺Id)获取互斥锁(本质是插入key),实现缓存重构// 调用Thread的sleep方法会抛出异常,可以使用try/catch/finally把获取锁和释放锁的过程包裹起来Shop shop = null;try {// 4.1 获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);// 4.2 判断是否获取锁成功(插入key是否成功)if(!isLock){//4.3 获取锁失败(插入key失败),则休眠一段时间重新查询商铺缓存(递归)Thread.sleep(50);return queryWithMutex(id);}//4.4 获取锁成功(插入key成功),则根据店铺的Id查询数据库shop = getById(id);// 由于本地查询数据库较快,这里可以模拟重建延时触发并发冲突Thread.sleep(200);// 5.在数据库中查不到对应的店铺则将空字符串写入Redis同时设置有效期if(shop == null){stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}//6.在数据库中查到了店铺信息即shop不为null,将shop对象转化为json字符串写入redis并设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.不管前面是否会有异常,最终都必须释放锁unlock(lockKey);}// 最终把查询到的商户信息返回给前端return shop;
}
测试互斥锁解决缓存击穿
使用Jmeter
模拟缓存击穿情景,在某时刻一个热点店铺的缓存的TTL到期了,此时用户不能从Redis中获取热点店铺的缓存数据,然后就都得去数据库里查询店铺信息
-
首先
将Redis中的热点店铺的缓存数据删除模拟TTL到期
,然后使用Jmete开100个线程来访问这个没有缓存的店铺信息 -
如果后台日志只输出了一条SQL语句则说明我们的互斥锁是生效的,没有造成大量用户都去数据库执行SQL语句查询店铺的信息
PLAINTEXT
: ==> Preparing: SELECT id,name,type_id,images,area,address,x,y,avg_price,sold,comments,score,open_hours,create_time,update_time FROM tb_shop WHERE id=?
: ==> Parameters: 2(Long)
: <== Total: 1
逻辑过期(缓存预热)
缓存击穿问题主要原因是由于我们对key设置了过期时间,假设我们不设置过期时间其实就不会有缓存击穿的问题,但是不设置过期时间,缓存数据又会一直占用内存
- 优点: 通过异步线程构建缓存,避免其他线程出现等待,提高了性能
- 缺点: 构建异步线程业务复杂,需要维护一个expire字段需要额外内存消耗, 在异步线程构建完缓存之前,其他线程返回的都是过期的数据(脏数据)导致数据不一致
逻辑过期应用
实现根据店铺Id查询商铺
的业务,基于逻辑过期方式(需要提前添加热点key)来解决缓存击穿问题
第一步: 因为现在redis中存储的数据的value需要带上过期时间属性,可以新建一个实体类包含原有的数据和过期时间字段
(不侵入原来代码)
@Data
public class RedisData {// 过期时间private LocalDateTime expireTime// 原有数据(用万能的Object) private Object data;
}
第二步: 在ShopServiceImpl中新增一个方法,利用单元测试进行缓存预热即添加热点key
,将热点店铺信息和过期时间字段封装到RedisData对象中并写入Redis缓存中
public void saveShop2Redis(Long id, Long expirSeconds) {// 1.根据店铺Id去数据库中查询店铺数据Shop shop = getById(id);// 由于本地查询数据库较快,模拟重建延时Thread.sleep(200);// 2.封装逻辑过期时间(当前时间转换为秒)RedisData redisData = new RedisData();// 设置热点店铺信息redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));// 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
第三步: 在测试类中运行测试方法,然后去Redis图形化页面查看存入的value
(含有data字段即shop对象和expireTime逻辑过期时间字段)
@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate ShopServiceImpl shopService;@Testpublic void test(){shopService.saveShop2Redis(1L,1000L);}
}
{"data": {"area": "大关","openHours": "10:00-22:00","sold": 4215,"images": "https://qcloud.dpfile.com/pc/jiclIsCKmOI2arxKN1Uf0Hx3PucIJH8q0QSz-Z8llzcN56-IdNpm8K8sG4.jpg","address": "金华路锦昌文华苑29号","comments": 3035,"avgPrice": 80,"updateTime": 1666502007000,"score": 37,"createTime": 1640167839000,"name": "476茶餐厅","x": 120.149192,"y": 30.316078,"typeId": 1,"id": 1},"expireTime": 1666519036559
}
第四步: 编写queryWithLogicalExpire
方法,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById
方法中做统一判断并返回结果类
//声明一个线程池,因为使用逻辑过期解决缓存击穿的方式需要新建一个线程来完成重构缓存
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);@Override
public Result queryById(Long id) { // 测试使用逻辑过期的方式解决缓存击穿Shop shop = queryWithLogicalExpire(id);// 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串if (shop == null) {return Result.fail("店铺不存在!!");}// shop不等于null,把查询到的商户信息返回给前端return Result.ok(shop);
}public Shop queryWithLogicalExpire(Long id) {//1.先从Redis中查询对应的热点店铺缓存信息(包含过期时间),这里的常量值是固定的店铺前缀+查询店铺的IdString json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.如果未命中即json等于null或命中了但json等于空字符串直接返回null(说明我们没有导入对应的key)//""和null以及"/t/n(换行)"都会判定为空即返回falseif (StrUtil.isBlank(json)) {return null;}//3.如果在Redis中查询到了热点店铺信息并且不是空字符串,则将JSON字符串转化为RedisData对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//4.redisData.getData()的本质类型是JSONObject类型(还是JSON字符串)并不是Object类型对象,所以不能直接强转为Shop类型,需要使用工具类Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//5.获取RedisData对象中封装的过期时间,判断是否过期LocalDateTime expireTime = redisData.getExpireTime();if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息 return shop;}// 6.已过期,需要缓存重建,查询数据库对应的店铺信息然后写入Redis同时设置逻辑过期时间// 6.1.获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);// 6.2.判断是否获取锁成功if (isLock){// 再次检测Redis缓存是否过期(双重检查),如果存在则无需重建缓存// 如果Redis中缓存的店铺信息还是过期,开启独立线程,实现缓存重建(测试的时候可以休眠200ms),实际中缓存的逻辑过期时间设置为30分钟CACHE_REBUILD_EXECUTOR.submit( ()->{// 开启独立线程try{this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(LOCK_SHOP_KEY + id);}});}// 6.4.返回过期的商铺信息return shop;
}public void saveShop2Redis(Long id, Long expirSeconds) {// 1.根据店铺Id去数据库中查询店铺数据Shop shop = getById(id);// 由于本地查询数据库较快,模拟重建延时Thread.sleep(200);// 2.封装逻辑过期时间(当前时间转换为秒)RedisData redisData = new RedisData();// 设置热点店铺信息redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));// 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
测试逻辑过期解决缓存击穿
使用Jmeter进行测试所有的线程查不到数据时是否都会执行缓存重建还是返回旧数据,重建数据时如果数据不一致会不会更新Redis中的缓存数据
- 在测试类
HmDianPingApplicationTests
中使用saveShop2Redis
方法,向Redis中添加一个热点店铺信息的缓存同时设置逻辑过期时间为2秒 - 在MySQL数据库中手动修改这个热点店铺的信息,2秒后Redis中缓存的热点店铺数据逻辑过期且和MySQL数据库中对应的店铺信息不一致
- 当用户访问到过期的缓存数据的时候就需要来新开一个线程重构缓存数据,在重构之前只能获得脏数据(修改前的数据),重构完后才能获得新数据(修改后的数据)
开100个去访问逻辑过期数据
前面的用户只能看到脏数据,后面的用户看到的才是新数据
相关文章:

黑马点评-07缓存击穿问题(热点key失效)及解决方案,互斥锁和设置逻辑过期时间
缓存击穿问题(热点key失效) 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且重建缓存业务较复杂的key突然失效了,此时无数的请求访问会在瞬间打到数据库,带来巨大的冲击 一件秒杀中的商品的key突然失效了,由于大家都在疯狂抢购那么这个瞬间就会有无数的请求…...

信息系统项目管理师第四版学习笔记——项目进度管理
项目进度管理过程 项目进度管理过程包括:规划进度管理、定义活动、排列活动顺序、估算活动持续时间、制订进度计划、控制进度。 规划进度管理 规划进度管理是为规划、编制、管理、执行和控制项目进度而制定政策、程序和文档的过程。本过程的主要作用是为如何在…...
指挥棒:C++ 与运算符
文章目录 参考描述算术运算符除法运算取模运算复合赋值运算符自增运算符自减运算符 比较运算符逻辑运算符概念短路为什么需要短路机制? 参考 项目描述微软C 语言文档搜索引擎Bing、GoogleAI 大模型文心一言、通义千问、讯飞星火认知大模型、ChatGPTC Primer Plus &…...

HTTPS建立连接的过程
HTTPS 协议是基于 TCP 协议的,因而要先建立 TCP 的连接。在这个例子中,TCP 的连接是在手机上的 App 和负载均衡器 SLB 之间的。 尽管中间要经过很多的路由器和交换机,但是 TCP 的连接是端到端的。TCP 这一层和更上层的 HTTPS 无法看到中间的包…...

Python接口自动化搭建过程,含request请求封装!
开篇碎碎念 接口测试自动化好处 显而易见的好处就是解放双手😀。 可以在短时间内自动执行大量的测试用例通过参数化和数据驱动的方式进行测试数据的变化,提高测试覆盖范围快速反馈测试执行结果和报告支持持续集成和持续交付的流程 使用Requestspytes…...

Vue3 编译原理
文章目录 一、编译流程1. 解读入口文件 packgages/vue/index.ts2. compile函数的运行流程 二、AST 解析器1. ast 的生成2. 创建ast的根节点3. 解析子节点 parseChildren(关键)4. 解析模版元素 Element模版元素解析-举例分析 一、编译流程 1. 解读入口文…...
spring boot整合Minio
MinIO 安装MinIo # 先创建minio 文件存放的位置 mkdir -p /opt/docker/minio/data# 启动并指定端口 docker run \-p 9000:9000 \-p 5001:5001 \--name minio \-v /opt/docker/minio/data:/data \-e "MINIO_ROOT_USERminioadmin" \-e "MINIO_ROOT_PASSWORDmini…...

Hadoop----Azkaban的使用与一些报错问题的解决
1.因为官方只放出源码,并没有放出其tar包,所以需要我们自己编译,通过查阅资料我们可以使用gradlew对其进行编译,还是比较简单,然后将里面需要用到的服务文件夹进行拷贝,完善其文件夹结构,通常会…...

「新房家装经验」客厅电视高度标准尺寸及客厅电视机买多大尺寸合适?
客厅电视悬挂高度标准尺寸是多少? 客厅电视悬挂高度通常在90~120厘米之间,电视挂墙高度也可以根据个人的喜好和实际情况来调整,但通常不宜过高,以坐在沙发上观看时眼睛能够平视到电视中心点或者中心稍微往下一点的位置为适宜。 客…...
ArduPilot开源飞控之AP_Baro_DroneCAN
ArduPilot开源飞控之AP_Baro_DroneCAN 1. 源由2. back-end抽象类3. 方法实现3.1 probe3.2 update3.3 subscribe_msgs3.4 handle_pressure/handle_temperature3.5 CAN port 4. 参考资料 1. 源由 鉴于ArduPilot开源飞控之AP_Baro中涉及Sensor Driver有以下总线类型: …...

Supervised Contrastive Pre-training for Mammographic Triage Screening Model
方法 品红色箭头表示将生成的孪生编码器分别迁移到单视角学习模块和双视角学习模块...
JVM技术文档--JVM优化思路以及问题定位--JVM可调整参数汇总
阿丹: 一个优秀的程序员,是因为在线上的排查以及遇到的线上、生产事故较多所以定位问题以及解决问题会比普通程序员快很多,所以一个优秀的程序员要逐渐形成自己的方法论,来完善和解决问题。 我们是如何发现问题的呢? …...
Oracle10g数据库迁移方案
试验了很多次Oracle数据库迁移才成功,贴出来给大家参考一下,我看到有的地方写迁移之后还需要重新建立temp表空间,这个还没有研究。另外说一点的是两个数据库的版本一定要一致,之前失败过一次,就是因为两个数据库的版本…...
备忘录模式:对象状态的保存与恢复
欢迎来到设计模式系列的第十八篇文章,本篇将介绍备忘录模式。备忘录模式是一种行为型设计模式,它允许在不破坏封装性的前提下捕获一个对象的内部状态,并在之后恢复该状态。这种模式通常用于需要提供撤销操作的情况。 什么是备忘录模式&#…...
C# InvokeRequired线程安全
C# InvokeRequired线程安全 为了保证新家的线程可能要对主界面的控件元素的属性发生一些改变,此时防止此操作对于主线程的影响,就提出了 InvokeRequired方法,保证主线程的安全,同时新加的线程也可以改变主页面中元素的值。 定义…...

pdf怎么转成jpg图片格式
pdf怎么转成jpg图片格式?对于大家平时在工作或者生活中的图片使用习惯,经常需要将各种格式的文件转换成易于浏览和使用的JPG格式图片以便保存。如今,因为pdf文件具有更强的稳定性和设备兼容性,PDF文件在平时的电脑使用过程中可以说…...

React +ts + babel+webpack
babel babel/preset-typescript 专门处理ts "babel/cli": "^7.17.6", "babel/core": "^7.17.8", "babel/preset-env": "^7.16.11", "babel/preset-react": "^7.16.7", "babel/preset…...

红队专题-REVERSE二进制逆向反编译
红队专题 招募六边形战士队员IDA pro安装python2加入环境变量py2安装pip安装IDA 7.0 proIDAPython: importing "site" failed. 招募六边形战士队员 一起学习 代码审计、安全开发、web攻防、逆向等。。。 私信联系 IDA pro 安装python2 python-2.7.3.msi 加入环…...
Spring技术原理之Bean生命周期原理解析
Spring技术原理之Bean生命周期原理解析 Spring作为Java领域中的优秀框架,其核心功能之一是依赖注入和生命周期管理。其中,Bean的生命周期管理是Spring框架中一个重要的概念。在本篇文章中,我们将深入探讨Spring技术原理中的Bean生命周期原理…...

Unity实现设计模式——模板方法模式
Unity实现设计模式——模板方法模式 模板模式(Template Pattern), 指在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。 简单说, 模板方法模式定义一个操作中的算法的骨架&…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...