【Redis场景3】缓存穿透、击穿问题
场景问题及原因
缓存穿透:
原因:客户端请求的数据在缓存和数据库中不存在,这样缓存永远不会生效,请求全部打入数据库,造成数据库连接异常。
解决思路:
缓存空对象
- 对于不存在的数据也在Redis建立缓存,值为空,并设置一个较短的TTL时间
- 问题:实现简单,维护方便,但短期的数据不一致问题
缓存雪崩:
原因:在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决思路:给不同的Key的TTL添加随机值(简单),给缓存业务添加降级限流策略(复杂),给业务添加多级缓存(复杂)
缓存击穿(热点Key):
前提条件:热点Key&在某一时段被高并发访问&缓存重建耗时较长
原因:热点key突然过期,因为重建耗时长,在这段时间内大量请求落到数据库,带来巨大冲击
解决思路:
互斥锁
- 给缓存重建过程加锁,确保重建过程只有一个线程执行,其它线程等待
- 问题:线程阻塞,导致性能下降且有死锁风险
逻辑过期
- 热点key缓存永不过期,而是设置一个逻辑过期时间,查询到数据时通过对逻辑过期时间判断,来决定是否需要重建缓存;重建缓存也通过互斥锁保证单线程执行,但是重建缓存利用独立线程异步执行,其它线程无需等待,直接查询到的旧数据即可
- 问题:不保证一致性,有额外内存消耗且实现复杂
场景问题实践解决
完整代码地址:https://github.com/xbhog/hm-dianping
分支:20221221-xbhog-cacheBrenkdown
分支:20230110-xbhog-Cache_Penetration_Avalance
缓存穿透:
代码实现:
public Shop queryWithPassThrough(Long id){
//从redis查询商铺信息
String shopInfo = stringRedisTemplate.opsForValue().get(SHOP_CACHE_KEY + id);
//命中缓存,返回店铺信息
if(StrUtil.isNotBlank(shopInfo)){
return JSONUtil.toBean(shopInfo, Shop.class);
}
//redis既没有key的缓存,但查出来信息不为null,则为空字符串
if(shopInfo != null){
return null;
}
//未命中缓存
Shop shop = getById(id);
if(Objects.isNull(shop)){
//将null添加至缓存,过期时间减少
stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY+id,"",5L, TimeUnit.MINUTES);
return null;
}
//对象转字符串
stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
return shop;
}
上述流程图和代码非常清晰,由于缓存雪崩简单实现(复杂实践不会)增加随机TTL值,缓存穿透和缓存雪崩不过多解释。
缓存击穿:
缓存击穿逻辑分析:
首先线程1
在查询缓存时未命中,然后进行查询数据库并重建缓存。注意上述缓存击穿发生的条件,被高并发访问&缓存重建耗时较长;
由于缓存重建耗时较长,在这时间穿插线程2,3,4
进入;那么这些线程都不能从缓存中查询到数据,同一时间去访问数据库,同时的去执行数据库操作代码,对数据库访问压力过大。
互斥锁:
解决方式:加锁;****可以采用**tryLock方法 + double check**
来解决这样的问题
在线程2
执行的时候,由于线程1
加锁在重建缓存,所以线程2
被阻塞,休眠等待线程1
执行完成后查询缓存。由此造成在重建缓存的时候阻塞进程,效率下降且有死锁的风险。
private Shop queryWithMutex(Long id) {
//从redis查询商铺信息
String shopInfo = stringRedisTemplate.opsForValue().get(SHOP_CACHE_KEY + id);
//命中缓存,返回店铺信息
if(StrUtil.isNotBlank(shopInfo)){
return JSONUtil.toBean(shopInfo, Shop.class);
}
//redis既没有key的缓存,但查出来信息不为null,则为空字符串
if(shopInfo != null){
return null;
}
//实现缓存重建
String lockKey = "lock:shop:"+id;
Shop shop = null;
try {
Boolean aBoolean = tryLock(lockKey);
if(!aBoolean){
//加锁失败,休眠
Thread.sleep(50);
//递归等待
return queryWithMutex(id);
}
//获取锁成功应该再次检测redis缓存是否还存在,做doubleCheck,如果存在则无需重建缓存。
synchronized (this){
//从redis查询商铺信息
String shopInfoTwo = stringRedisTemplate.opsForValue().get(SHOP_CACHE_KEY + id);
//命中缓存,返回店铺信息
if(StrUtil.isNotBlank(shopInfoTwo)){
return JSONUtil.toBean(shopInfoTwo, Shop.class);
}
//redis既没有key的缓存,但查出来信息不为null,则为“”
if(shopInfoTwo != null){
return null;
}
//未命中缓存
shop = getById(id);
// 5.不存在,返回错误
if(Objects.isNull(shop)){
//将null添加至缓存,过期时间减少
stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY+id,"",5L, TimeUnit.MINUTES);
return null;
}
//模拟重建的延时
Thread.sleep(200);
//对象转字符串
stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
return shop;
}
在获取锁失败时,证明已有线程在重建缓存,使当前线程休眠并重试(递归实现)。
代码中需要注意的是synchronized关键字的使用,在获取到锁的时候,在判断下缓存是否存在(失效)double-check
,该关键字锁的是当前对象。在其关键字{}
中是同步处理。
推荐博客:https://blog.csdn.net/u013142781/article/details/51697672
然后进行测试代码,进行压力测试(jmeter)
,首先去除缓存中的值,模拟缓存失效。
设置1000
个线程,多线程执行间隔5s
。
所有的请求都是成功的,其qps
大约在200
,其吞吐量还是比较可观的。然后看下缓存是否成功(只查询一次数据库);
逻辑过期:
思路分析:
当用户开始查询redis
时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value
取出,判断value
中的过期时间是否满足,如果没有过期,则直接返回redis
中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
封装数据:这里我们采用新建实体类来实现
/**
* @author xbhog
* @describe:
* @date 2023/1/15
*/
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
使得过期时间和数据有关联关系,这里的数据类型是Object
,方便后续不同类型的封装。
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.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.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){
exectorPool().execute(() -> {
try {
//重建缓存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return shop;
}
当前的执行流程跟互斥锁基本相同,需要注意的是,在获取锁成功后,我们将缓存重建放到线程池中执行,来异步实现。
线程池代码:
/**
* 线程池的创建
* @return
*/
private static ThreadPoolExecutor exectorPool() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
//根据自己的处理器数量+1
Runtime.getRuntime().availableProcessors()+1,
2L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
return executor;
}
缓存重建代码:
/**
* 重建缓存
* @param id 重建ID
* @param l 过期时间
*/
public void saveShop2Redis(Long id, long l) {
//查询店铺信息
Shop shop = getById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(l));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
测试条件:100
线程,1s
线程间隔时间,缓存失效时间10s
。
测试环境:缓存中存在对应的数据,并且在缓存快失效之前修改数据库中的数据,造成缓存与数据库不一致,通过执行压测,来查看相关线程返回的数据情况。
从上述两张图中可以看到,在前几个线程执行过程中店铺name为102
,当执行时间从19-20
的时候店铺name
发生变化为105
,满足逻辑过期异步执行缓存重建的需求.
【Redis场景3】缓存穿透、击穿问题的更多相关文章
- NoSQL & Redis 介绍、缓存穿透 & 击穿 & 雪崩
1. NoSql 简介 2. Redis 简介 2.1 Redis 的起源 2.2 缓存过期 & 缓存淘汰 3. 缓存异常 1)缓存穿透 2)缓存击穿 3)缓存雪崩 4)总结 1. NoSQL ...
- Redis中几个简单的概念:缓存穿透/击穿/雪崩,别再被吓唬了
Redis中几个“看似”高大上的概念,经常有人提到,某些好事者喜欢死扣概念,实战没多少,嘴巴里冒出来的全是高大上的名词,个人一向鄙视概念党,呵呵! 其实这几个概念:缓存穿透/缓存击穿/缓存雪崩,有一个 ...
- Redis系列(八)--缓存穿透、雪崩、更新策略
1.缓存更新策略 1.LRU/LFU/FIFO算法剔除:例如maxmemory-policy 2.超时剔除,过期时间expire,对于一些用户可以容忍延时更新的数据,例如文章简介内容改了几个字 3.主 ...
- Redis缓存穿透、缓存击穿以及缓存雪崩
作为一个内存数据库,redis也总是免不了有各种各样的问题,这篇文章主要是针对其中三个问题进行讲解:缓存穿透.缓存击穿和缓存雪崩.并给出一些解决方案.这三个问题是基本问题也是面试常问问题. 这篇文章我 ...
- Redis缓存雪崩,缓存穿透,热点key解决方案和分析
缓存穿透 缓存系统,按照KEY去查询VALUE,当KEY对应的VALUE一定不存在的时候并对KEY并发请求量很大的时候,就会对后端造成很大的压力. (查询一个必然不存在的数据.比如文章表,查询一个不存 ...
- 8.了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透?
作者:中华石杉 面试题 了解什么是 redis 的雪崩.穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? 面试官心理分析 其实这是问到缓存必问的,因为缓 ...
- 什么是 redis 的雪崩、穿透和击穿?
缓存雪崩 对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机.缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据 ...
- Java高并发缓存架构,缓存雪崩、缓存穿透之谜
面试题 了解什么是 redis 的雪崩.穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? 面试官心理分析 其实这是问到缓存必问的,因为缓存雪崩和穿透,是 ...
- Redis实现分布式缓存
Redis 分布式缓存实现(一) 1. 什么是缓存(Cache) 定义:就是计算机内存中的一段数据: 2. 内存中数据特点 a. 读写快 b. 断电立即丢失 3. 缓存解决了什么问题? a. 提 ...
- Redis缓存雪崩、缓存穿透、缓存击穿、缓存降级、缓存预热、缓存更新
Redis缓存能够有效地加速应用的读写速度,就DB来说,Redis成绩已经很惊人了,且不说memcachedb和Tokyo Cabinet之流,就说原版的memcached,速度似乎也只能达到这个级别 ...
随机推荐
- nrf9160 做modem—— 连接云(接入方式MQTT)
今天测试把nrf9160作为modem的例程Serial LTE Modem程序(后面简称slm),何为做modem,通俗来说就是将nrf9160作为无线模块,主控由其余MCU做,主控通过AT命令控制 ...
- 九、kubernetes命令行工具kubectl
为了方便在命令行下对集群.节点.pod进行管理,kubernetes官方提供了一个管理命令:kubectl kubectl作为客户端CLI工具,可以让用户通过命令行对Kubernetes集群进行操作. ...
- JS学习笔记 (四) 数组进阶
1.基本知识 1.数组是值的有序集合.每个值叫做一个元素,而每个元素在数组中的位置称为索引,以数字表示,以0开始. 2.数组是无类型的.数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的 ...
- 方法的重载(Overload)+ println重载
方法的重载(Overload) package cn.day01; /*方法的重载(Overload):多个方法的名称一样,但是参数列表不一样. * 好处:只需要记住唯一一个方法名称,就可以实现类似多 ...
- django启动报错:DisallowedHost at /
学习django第一天,第一次启动服务就报错,报错内容如下: DisallowedHost at / Invalid HTTP_HOST header: '192.168.116.22:8000'. ...
- C#使用Task在Winform建立控件上的提示等待窗口,实现局部等待加载,不影响主线程(二)
效果图: 源码:(处理了亿点点细节) 链接:https://pan.baidu.com/s/18S1IgQBOyXgeGvhnU3nrKQ?pwd=jpq9提取码:jpq9 作者:兮去博客出处:htt ...
- 微信小程序根据开发环境切换域名
domain.js // 获取当前账号信息,线上小程序版本号仅支持在正式版小程序中获取,开发版和体验版中无法获取. // envVersion:'develop','trial','release' ...
- ES文件传输助手1.0.0
软件下载地址 1.软件功能 与 ES文件浏览器 的快传功能 直接传输文件 支持接受文件点击预览 可以多台电脑使用该软件,从而实现电脑与电脑局域网互传文件 单个文件夹上传会递归上传该文件夹下所有文件夹与 ...
- 下载安装MinGW-w64详细步骤
一.MinGW-w64介绍 MinGW 的全称是:Minimalist GNU on Windows ,实际上是将gcc(c/c++编译器)移植到了 Windows 平台下,并且包含了 Win32AP ...
- go操作Kfaka
目录 1. Kafka介绍 1.1.1. Kafka是什么 1.1.2. Kafka的特点 1.1.3. 常用的场景 1.1.4. Kafka中包含以下基础概念 1.1.5. 消息 1.1.6. 消息 ...