给缓存过期时间加上随机扰动,同时用本地缓存做进程内兜底,再配合限流降级,数据库才扛得住瞬时洪峰。
缓存雪崩为什么会发生:从一次大促秒杀说起
想象一下,某电商平台在双11零点,首页商品详情页的缓存key全部设置了相同的过期时间比如都是30分钟,30分钟一到,几十万个key同时失效,下一波请求全部穿透缓存,直接砸向后端数据库,数据库连接池瞬间耗尽,整个服务雪崩。
这不是假设,是多数高并发系统迟早会踩的坑,缓存雪崩的本质不是缓存挂了,而是缓存集体失效,失效的原因通常有三种:
- 大量key设置了相同的过期时间,到期后同时消失。
- 缓存服务本身宕机或重启,比如Redis主节点挂了,从节点还没切换完成。
- 网络抖动导致缓存批量不可用。
其中第一种最隐蔽,因为平时流量低,数据库扛得住短暂穿透;一旦到了大促、秒杀、热点事件,问题就会被放大。
缓存雪崩怎么解决:三层防线拆解
解决缓存雪崩不能靠单一手段,得从缓存写入、缓存读取、数据库保护三个层面同时下手,下面按优先级拆开讲。
第一层:给过期时间加随机扰动
这是最直接、成本最低的一步,核心思路是不要让key在同一秒集体过期。
具体操作很简单:在设置缓存过期时间时,给基础TTL加上一个随机值,比如基础过期时间是30分钟,那么实际过期时间可以设为30分钟加上0到5分钟之间的随机数。
以Redis为例:
# 基础TTL为1800秒,加一个0到300秒的随机值 SET product:1001:detail "..." EX 1800 + RANDOM(0,300)
如果用Java代码,可以这样写:
int baseTtl = 1800; int randomTtl = new Random().nextInt(300); redisTemplate.opsForValue().set(key, value, baseTtl + randomTtl, TimeUnit.SECONDS);
也可以用Redis自带的SETEX配合一个随机过期时间,但更推荐在业务代码里统一封装一个setWithRandomTtl方法,团队里所有人写缓存都走这个方法,能避免遗漏。
为什么随机值有效? 因为即使某个时间点有key过期,也只是少量key过期,数据库面对的请求量是平滑的,不会形成尖峰。
第二层:本地缓存兜底,挡住最后一波穿透
随机过期时间解决了“同时过期”的问题,但解决不了“缓存服务整体不可用”的场景,比如Redis主从切换的那几秒,或者机房网络抖动,这时候就需要
本地缓存出来扛一下。
本地缓存是指应用进程内部的缓存,比如Java里的Caffeine、Guava Cache,或者是Go语言里的bigcache,它不依赖网络,读取速度快,但容量有限,且各实例之间数据不共享。
本地缓存和redis缓存哪个好? 这不是二选一的问题,而是两者配合,Redis负责全局共享、容量大、一致性相对好;本地缓存负责极端情况下的兜底,哪怕Redis完全不可用,也能用旧数据撑住关键接口。
在缓存雪崩场景下,本地缓存的兜底逻辑可以这样设计:
- 业务代码先查本地缓存,命中直接返回。
- 本地缓存未命中,再查Redis。
- Redis查询失败或超时,不要直接抛异常,而是尝试返回本地缓存的旧值(如果存在)。
- 如果本地缓存也没有,才走数据库查询,查询成功后写入本地缓存和Redis。
这样即使Redis挂了,本地缓存里只要存有最近访问过的热点数据,请求就不会全部压到数据库。
第三层:限流、降级、熔断,给数据库留条活路
前两层属于“预防”,第三层属于“止损”,如果前两层都失效了,比如本地缓存刚好也过期了,那只能靠限流和降级来保护数据库。
常见的做法是:
- 接口限流:用令牌桶或漏桶算法,比如Guava RateLimiter或Sentinel,限制每秒打到数据库的请求数。
- 降级策略:当检测到Redis连接异常或数据库负载过高时,直接返回一个兜底数据或静态页面,比如商品详情页返回“系统繁忙,请稍后再试”的JSON,而不是让请求继续往后端打。
- 熔断机制:像Hystrix或Resilience4j,当某个下游依赖的错误率超过阈值时,自动打开熔断器,快速失败。
这三层组合起来,基本能把缓存雪崩的破坏力控制在可接受范围内。
缓存雪崩和缓存穿透的区别:别把两个概念搞混
很多开发者在排查线上问题时,会把缓存雪崩和缓存穿透说成一件事,实际上它们的触发条件和应对方式完全不同。
用一个表格对比一下:
| 对比维度 | 缓存雪崩 | 缓存穿透 |
|---|---|---|
| 触发条件 | 大量key同时过期或缓存服务不可用 | 请求的key在数据库里根本不存在 |
| 典型场景 | 双11零点热点key集体过期 | 恶意请求不存在的商品ID |
| 数据库压力 | 瞬时洪峰,但每个请求可能查到数据 | 每次请求都查不到数据,可能无数次穿透 |
| 核心解法 | 随机过期时间、本地缓存兜底、限流降级 | 布隆过滤器、空值缓存、参数校验 |
| 本地缓存是否有效 | 有效,能挡住热点数据 | 对不存在的数据无效,除非缓存空值 |
一句话总结:缓存雪崩是“缓存大面积失效”,缓存穿透是“请求的数据本来就没有”。 预防措施不能混用,比如给key加随机过期时间对缓存穿透毫无帮助,因为穿透的key根本没有缓存。
电商大促缓存雪崩场景下的预防清单
如果你正在负责一个电商大促项目,比如双11、618或者某个品牌日,下面这份清单可以直接拿来用,按执行顺序排列。
上线前一周:代码与配置检查
- [ ] 检查所有写入Redis的代码,确认过期时间都使用了随机扰动函数。
- [ ] 对核心接口(商品详情、购物车、订单确认)接入本地缓存,比如Caffeine。
- [ ] 配置好限流规则,比如核心接口QPS限制。
- [ ] 准备降级开关,能够一键切换返回静态兜底数据。
上线前一天:压测与应急预案
- [ ] 用压测工具模拟Redis主节点宕机,观察本地缓存是否能兜住。
- [ ] 验证限流和熔断阈值是否合理,避免误伤正常流量。
- [ ] 确保运维监控告警配置好了Redis内存使用率、连接数、命中率等指标。
大促当天:实时观察与快速响应
- [ ] 重点关注Redis的key过期分布,使用
INFO stats查看expired_keys的增长情况。 - [ ] 观察数据库连接池使用率,如果超过危险水位,立即打开限流。
- [ ] 如果发现某个热点key过期后数据库压力激增,可以手动执行
SET命令重新预热缓存。
本地缓存框架怎么选:成本与效果的平衡
提到本地缓存,Java开发者经常会问:Caffeine、Guava Cache、Ehcache,到底用哪个?其实多数场景下,Caffeine是最稳妥的选择,它的命中率和淘汰算法比Guava Cache更优,而且API类似,迁移成本低。
如果团队里有Go服务,可以用freecache或bigcache,都是比较成熟的本地缓存库。
价格和成本方面,本地缓存本身是免费的,不涉及额外付费,但要注意它占用应用进程的内存,一个实例分配256MB给本地缓存,对于大多数中小型服务足够,如果实例内存本身就紧张,可以调低到128MB,只缓存最热的数据。
在“北京、上海、杭州这些互联网公司密集的地区”,很多团队会把本地缓存和Redis的容量做阶梯配置:本地缓存只存最热的1%数据,Redis存全量热数据,数据库存冷数据,这样每一层的成本都能控制住。
写在最后
缓存雪崩不是无法预防的黑天鹅,而是可以通过随机过期时间、本地缓存兜底、限流降级这三板斧解决的工程问题,关键是平时就把这些机制做进代码模板里,而不是等到大促当晚临时加配置,系统稳定从来不是靠运气,而是靠那些看起来不起眼的小改动叠加出来的。
Q&A:关于缓存雪崩预防与本地缓存兜底
缓存雪崩预防中,本地缓存兜底策略有哪些常见误区?
最常见的误区有两个,一是把本地缓存当成Redis的替代品,试图把所有数据都塞进本地缓存,导致应用内存暴涨甚至OOM,二是认为本地缓存能解决所有穿透问题,实际上对于数据库里不存在的数据,本地缓存无能为力,除非你专门缓存空值,本地缓存的定位是热点数据的最后一道防线,容量和淘汰策略都要围绕这个定位来设计。
本地缓存和redis缓存哪个好?在不同地域部署时怎么选?
没有绝对的好与坏,Redis适合跨实例共享数据、需要持久化或主从高可用的场景;本地缓存适合极端情况下用旧数据兜底、对一致性要求不高的读多写少场景,在不同地域部署时,比如北京和上海两个机房,如果Redis跨机房延迟较高,可以在每个机房的实例内加重本地缓存,只允许本地缓存兜底,不允许跨机房回源到另一个机房的数据库。
缓存雪崩和缓存穿透的区别会影响本地缓存的配置吗?
会,缓存雪崩场景下,本地缓存应该配置较短的过期时间(比如1到5分钟),保证热点数据能及时更新,缓存穿透场景下,如果要做空值缓存,本地缓存的过期时间可以设得更短,避免大量不存在的key占用内存,两者的淘汰策略和容量规划完全不同,混淆配置会让问题更严重,实际项目中,先明确当前系统主要面临哪种风险,再决定本地缓存的参数。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/636167.html





