黑马点评-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), 指在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。 简单说, 模板方法模式定义一个操作中的算法的骨架&…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...
