Redis【缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题】

缓存策略详解
本文深入探讨缓存雪崩、穿透、预热、更新与降级等常见问题,提供多种解决方案,包括加锁、缓存标记、布隆过滤器等技术手段,旨在提升系统稳定性和性能。

摘之:https://blog.youkuaiyun.com/xlgen157387/article/details/79530877

今天给大家整理一篇关于Redis经常被问到的问题:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降
级等概念的入门及简单解决方案。

一、缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓
存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存
的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕
机。从而形成一系列连锁反应,造成整个系统崩溃

缓存正常从Redis中获取,示意图如下:

在这里插入图片描述
缓存失效瞬间示意图如下:
在这里插入图片描述

缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

以下简单介绍两种实现方式的伪代码:

(1) 碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪
代码如下:

//伪代码
public object GetProductListNew() {
	int cacheTime = 30;
	String cacheKey = "product_list";
	String lockKey = cacheKey;
	String cacheValue = CacheHelper.get(cacheKey);
	if (cacheValue != null) {
		return cacheValue;
	} else {
		synchronized(lockKey) {
		cacheValue = CacheHelper.get(cacheKey);
		if (cacheValue != null) {
			return cacheValue;
		} else {
		//这里一般是sql查询数据
			cacheValue = GetProductListFromDB();
			CacheHelper.Add(cacheKey, cacheValue, cacheTime);
		}
		return cacheValue;
	}
}

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间

key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意: 加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2) 还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

//伪代码
public object GetProductListNew() {
	int cacheTime = 30;
	String cacheKey = "product_list";
	//缓存标记
	String cacheSign = cacheKey + "_sign";
	String sign = CacheHelper.Get(cacheSign);
	//获取缓存值
	String cacheValue = CacheHelper.Get(cacheKey);
	if (sign != null) {
		return cacheValue; //未过期,直接返回
	} else {
		CacheHelper.Add(cacheSign, "1", cacheTime);
		ThreadPool.QueueUserWorkItem((arg) -> {
			//这里一般是 sql查询数据
			cacheValue = GetProductListFromDB();
			//日期设缓存时间的2倍,用于脏读
			CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
			});
		return cacheValue;
	}
}
解释说明:

1、 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

2、 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。

二、缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!

//伪代码
public object GetProductListNew() {
	int cacheTime = 30;
	String cacheKey = "product_list";
	String cacheValue = CacheHelper.Get(cacheKey);
	if (cacheValue != null) {
		return cacheValue;
	}
	cacheValue = CacheHelper.Get(cacheKey);
	if (cacheValue != null) {
		return cacheValue;
	} else {
		//数据库查询不到,为空
		cacheValue = GetProductListFromDB();
		if (cacheValue == null) {
			//如果发现为空,设置个默认值,也缓存起来
			cacheValue = string.Empty;
		}
		CacheHelper.Add(cacheKey, cacheValue, cacheTime);
		return cacheValue;
	}
}

把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。

三、缓存预热

缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决思路:

1、 直接写个缓存刷新页面,上线时手工操作下;

2、 数据量不大,可以在项目启动的时候自动进行加载;

3、 定时刷新缓存;

四、缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

(1) 定时去清理过期的缓存;
(2) 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

五、缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入
购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓
死保护,哪些可降级;比如可以参考日志级别设置预案:

(1) 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

(2) 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

(3) 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

(4) 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

六、总结

这些都是实际项目中,可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际上还有很多很多各种各样的问题,文中的解决方案,也不可能满足所有的场景,相对来说只是对该问题的入门解决方法。一般正式的业务场景往往要复杂的多,应用场景不同,方法和解决方案也不同,由于上述方案,考虑的问题并不是很全面,因此并不适用于正式的项目开发,但是可以作为概念理解入门,具体解决方案要根据实际情况来确定!

参考文章:

<think>我们正在讨论Redis缓存雪崩问题。根据引用内容,缓存雪崩是指大量缓存数据同时过期,导致大量请求直接访问数据库,造成数据库压力剧增甚至崩溃的现象。接下来,我将从原理、原因、解决方案三个方面进行详细说明。 ### 一、Redis缓存雪崩原理 **核心机制**: 当缓存中大量数据**在同一时间点过期**(如设置相同TTL),或Redis服务**突然宕机**,导致原本应由缓存处理的请求直接涌向数据库,引发数据库瞬时压力激增[^1][^2][^5]。 ```mermaid graph LR A[大量并发请求] --> B[Redis缓存集群] B -->|大量Key同时失效| C[数据库] C -->|瞬时高并发查询| D[数据库崩溃] ``` > 📌 **雪崩效应**:数据库压力过大导致响应变慢 → 应用线程阻塞 → 更多请求积压 → 系统全面瘫痪[^5]。 --- ### 二、主要原因分析 | **类型** | **具体场景** | **案例说明** | |------------------|-----------------------------------------------------------------------------|----------------------------------| | **集中过期** | 批量缓存设置相同过期时间(如00:00统一过期) | 促销活动结束瞬间所有优惠信息失效 | | **服务宕机** | Redis集群故障或网络中断 | 主从切换失败导致服务不可用 | | **热点依赖** | 系统过度依赖单一缓存集群 | 某业务线流量激增拖垮整个缓存 | | **冷启动** | 新系统上线时缓存预热 | 大促开始时缓存空载 | --- ### 三、解决方案及实战策略 #### 1. **分散过期时间** **核心思路**:避免大量Key同时失效 **实现方式**: ```java // 伪代码:基础过期时间 + 随机偏移量 int baseTTL = 3600; // 基础缓存时间1小时 int randomOffset = (int)(Math.random() * 600); // 随机0-10分钟 redis.setex(key, baseTTL + randomOffset, value); ``` **效果**:将Key的过期时间均匀分布在3600~4200秒区间[^1][^5]。 #### 2. **多级缓存架构** **分层设计**: ```mermaid graph TB A[客户端] --> B[本地缓存] B -->|未命中| C[Redis集群] C -->|未命中| D[数据库] ``` - **本地缓存**:Guava/Caffeine(应对Redis宕机) - **备份缓存**:Redis Cluster + 持久化存储(如MySQL) **优势**:即使Redis崩溃,本地缓存仍可支撑部分流量[^3][^5]。 #### 3. **限流降级** **关键措施**: - **限流**:使用Sentinel或Gateway限制数据库每秒查询数(QPS) ```bash # Sentinel规则示例 flowRule: resource: db_query count: 1000 # 数据库最大承受1000 QPS grade: 1 # QPS限流模式 ``` - **降级**:返回兜底数据(如默认商品列表)或错误页[^4][^5]。 #### 4. **热点数据永不过期** **策略**: - 逻辑过期:物理上不设置TTL,代码中异步更新 ```python def get_data(key): data = redis.get(key) if data.expire_time < now(): # 检查逻辑过期时间 thread_pool.submit(update_data, key) # 异步更新 return data.value ``` **适用场景**:高频访问的核心数据(如首页推荐)[^5]。 #### 5. **服务高可用** **架构设计**: - **Redis集群**:主从复制 + Sentinel自动故障转移 - **异地多活**:跨机房部署避免单点故障 --- ### 四、最佳实践组合 | **阶段** | **措施** | **目标** | |----------------|-------------------------------|------------------------------| | **预防期** | 分散过期时间 + 多级缓存 | 降低集中失效风险 | | **运行期** | 实时监控命中率 + 自动扩缩容 | 快速发现资源瓶颈 | | **崩溃时** | 限流降级 + 熔断机制 | 保障核心业务可用 | | **恢复期** | 缓存预热 + 数据分批次加载 | 避免二次雪崩 | > 💡 **监控指标**: > ```bash > redis-cli info stats | grep keyspace_misses # 缓存未命中次数 > mysqladmin status -uroot -p | grep Queries # 数据库实时查询量 > ``` --- §§相关问题§§ 1. 如何设计一个自动化的缓存预热系统? 2. 在微服务架构中,如何实现跨服务的多级缓存同步? 3. Redis集群和Sentinel在高可用方案中的具体协作机制是什么? 4. 除了随机过期时间,还有哪些方法可以分散缓存失效时间? 5. 如何评估缓存雪崩解决方案对系统性能的影响?</think>### Redis缓存雪崩:深度解析原理、原因与解决方案 --- #### 一、核心原理 **定义**: 当大量缓存数据**在同一时间点集中过期**,或Redis服务**突然宕机**,导致海量请求直接穿透到数据库,引发数据库瞬时压力激增甚至崩溃的现象[^1][^2][^5]。 ```mermaid graph LR A[大量并发请求] --> B[Redis缓存集群] B -->|大量Key同时失效| C[数据库] C -->|瞬时高并发查询| D[数据库崩溃] ``` > 📌 **雪崩效应**: > 数据库压力过大 → 响应延迟 → 应用线程阻塞 → 更多请求积压 → 系统全面瘫痪[^5]。 --- #### 二、根本原因 | **类型** | **典型场景** | **案例** | |------------------|-----------------------------------------------------------------------------|------------------------------| | **集中过期** | 批量缓存设置相同过期时间(如00:00统一过期) | 促销活动结束瞬间所有优惠信息失效 | | **服务宕机** | Redis集群故障/网络中断 | 主从切换失败导致服务不可用 | | **热点依赖** | 系统过度依赖单一缓存集群 | 某业务线流量激增拖垮整个缓存 | | **冷启动** | 新系统上线时缓存预热 | 大促开始时缓存空载 | --- #### 三、解决方案(分层防御) ##### 1. **预防层:分散失效时间** **核心逻辑**:避免大量Key同时过期 ```java // 伪代码:基础过期时间 + 随机偏移量 int baseTTL = 3600; // 基础缓存时间1小时 int randomOffset = (int)(Math.random() * 600); // 随机0-10分钟 redis.setex(key, baseTTL + randomOffset, value); ``` **效果**:将Key过期时间均匀分布在3600~4200秒区间[^1][^5]。 ##### 2. **架构层:多级缓存** **分层设计**: ```mermaid graph TB A[客户端] --> B[本地缓存] B -->|未命中| C[Redis集群] C -->|未命中| D[数据库] ``` - **本地缓存**:Guava/Caffeine(应对Redis宕机) - **备份缓存**:Redis Cluster + 持久化存储(如MySQL) **优势**:即使Redis崩溃,本地缓存仍可支撑部分流量[^3][^5]。 ##### 3. **应急层:限流熔断** **关键措施**: - **限流**:Sentinel/Gateway限制数据库QPS(如每秒1000次查询) ```bash # Sentinel规则示例 flowRule: resource: db_query count: 1000 # 数据库最大承受1000 QPS grade: 1 # QPS限流模式 ``` - **熔断**:Hystrix在数据库压力超阈值时返回兜底数据[^4][^5]。 ##### 4. **数据层:热点永不过期** **策略**: - 逻辑过期:物理上不设置TTL,代码中异步更新 ```python def get_data(key): data = redis.get(key) if data.expire_time < now(): # 检查逻辑过期时间 thread_pool.submit(update_data, key) # 异步更新 return data.value ``` **适用场景**:高频访问的核心数据(如首页推荐)[^5]。 ##### 5. **高可用层:集群容灾** **架构设计**: - **Redis集群**:主从复制 + Sentinel自动故障转移 - **异地多活**:跨机房部署避免单点故障 --- #### 四、最佳实践组合 | **场景** | **策略组合** | **目标** | |------------------|---------------------------------------------|------------------------------| | **新系统上线** | 缓存预热 + 分散过期时间 | 避免冷启动雪崩 | | **大促活动** | 多级缓存 + 热点永不过期 | 支撑瞬时高并发 | | **服务故障** | 限流熔断 + 本地缓存兜底 | 保障核心业务可用性 | | **长期运行** | 实时监控命中率 + 自动扩缩容 | 预防资源瓶颈 | > 💡 **关键监控指标**: > ```bash > redis-cli info stats | grep keyspace_misses # 缓存未命中次数 > mysqladmin status -uroot -p | grep Queries # 数据库实时查询量 > ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值