缓存雪崩的预防重点不在 Redis 本身,而在于让过期时间错开、给流量装一道闸门、给数据库留出喘息空间;本地缓存兜底的关键,是把它当“第二层盾牌”而不是“缩小版 Redis”,用很短的 TTL 加广播失效来换一致性。
雪崩不是缓存挂了,是缓存同时“下班”了
先还原一个典型现场,晚上八点,电商大促流量爬坡,代码里所有商品详情 key 都设了 24h 过期因为当初是在早上八点做预热,于是第二天早上八点整,几十万个 key 在同一秒集体失效,Redis 命中率从九成多直接掉到接近 0,请求像退潮后裸露的礁石一样全部砸在 MySQL 上,连接池在几十毫秒内被打满,接口超时,上游重试,流量再翻一倍,最后整条链路雪崩。
业内把这类故障归为三种触发路径:
- 批量同刻过期,最常见,也最容易被忽略,往往来自一次“整齐”的预热脚本。
- 缓存层整体不可用,主从切换、集群扩容、机房网络抖动,都会让缓存瞬间“消失”。
- 实例重启后空缓存,发布、扩缩容、容器漂移,新节点冷启动时缓存是空的,等于把流量原样转给数据库。
行业共识认为,雪崩真正的杀伤力不在“缓存没了”,而在“放大”,一个原本被缓存挡住的请求会变成一次数据库查询,重试逻辑再把它乘以 2 到 3 倍,所以预防思路始终是两条:别让它们同时失效,别让失效后的流量原样穿透,据多家云厂商的公开技术文档,多级缓存加限流降级是应对这类故障的基础组合,而不是可选优化。
缓存雪崩和缓存击穿到底差在哪,先分清再谈预防
很多人把三个词混着用,结果对策也混着上,效果自然打折。
| 概念 | 触发条件 | 影响范围 | 典型现象 | 主要对策 |
|---|---|---|---|---|
| 缓存雪崩 | 大批 key 同时过期,或缓存层整体不可用 | 面,整个系统 | 数据库 QPS 陡增,连接池打满 | TTL 打散、多级缓存、限流降级 |
| 缓存击穿 | 单个热点 key 过期 | 点,单个商品或配置 | 某一行数据被反复查 | 互斥锁重建、逻辑过期 |
| 缓存穿透 | 查询本身不存在的 key | 面,且持续 | 大量空结果打到数据库 | 空值缓存、布隆过滤器 |
分清楚之后,对策就不会错位:雪崩要解决“,击穿要解决“并发重建”,穿透要解决“无效查询”,三者经常一起出现,但优先级不同,雪崩永远是第一优先,因为它一次性打掉的是整条链路。
高并发场景下缓存雪崩怎么预防:四道防线
第一道防线:让过期时间错开
最便宜、见效最快的一招,原理很简单,别让任何人写出“整点过期”的代码。
// 基础 TTL 1 小时,随机浮动 5 分钟 int ttl = 3600 + ThreadLocalRandom.current().nextInt(300); redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
几个可验证的落地细节:
- 偏移量按 key 数量估算,十万级 key 用正负 5 分钟就够,百万级建议放宽到 10 到 30 分钟。
- 预热脚本改造,把批量
EXPIRE改成逐条带随机值,别用一条 pipeline 统一设。 - 热点 key 单独处理,对访问量排名靠前的 key 采用逻辑过期:value 里存一个
expireAt字段,物理上永不过期,由异步线程刷新。
{"data": {...}, "expireAt": 1767225600000}
读取时先判断 expireAt,过期了就返回旧值并丢一个刷新任务到线程池,用户端感知不到抖动。
第二道防线:热点数据预热
发布、扩容、缓存重建之后,缓存是空的,这段时间最危险,做法是:
- 把访问量靠前的 key 清单落到配置中心或数据库。
- 应用启动完成后,通过
ApplicationRunner异步批量回填,注意加并发度限制,别把数据库自己打挂。 - 定时任务每天低峰期做一次全量校正,避免长期漂移。
第三道防线:给流量装闸门
缓存不可用时,必须有人在前面拦,据简米云、酷番云公开的限流组件文档,常规做法是按“数据库能承受的 QPS 的较大比例”设置阈值,而不是按接口理论峰值。
- 限流:Sentinel 对核心接口配置 QPS 阈值,超出的请求快速失败。
- 熔断:Redis 客户端设置 50 毫秒以内的超时,连续失败达到阈值直接短路,不再等待。
- 降级:返回上一次的快照数据、默认值或静态页,保证“难看但可用”。
第四道防线:数据库别裸奔
- 连接池
maximumPoolSize按数据库实际并发能力设定,不是越大越好。 - 开启慢查询日志,雪崩期间第一时间能定位是谁在拖。
- 非核心查询走读库或异步队列削峰。
本地缓存兜底怎么做才靠谱
本地缓存兜底选 Caffeine 还是 Guava,怎么选不踩坑
直接给结论:新项目选 Caffeine,存量项目如果已经在用 Guava Cache 且没有性能痛点,不必急着重构,据 Guava 官方仓库的说明,其缓存组件已推荐新项目考虑 Caffeine,这是公开可查的信息。
| 维度 | Caffeine | Guava Cache |
|---|---|---|
| 淘汰算法 | W-TinyLFU,命中率更高 | LRU 近似 |
| 读写性能 | 高并发下优势明显 | 并发场景有锁竞争 |
| Spring Boot 集成 | 官方 starter 直接支持 | 需自定义 CacheManager |
| 维护状态 | 活跃 | 维护模式 |
| 适用场景 | 新项目、高 QPS 读场景 | 老项目、低频读 |
选型之外,参数才是关键,本地缓存是“兜底”,不是“主存”,容量和 TTL 都要收着给:
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=5s
maximumSize按堆内存算,单实例通常控制在几百 MB 以内。expireAfterWrite设 1 到 10 秒,越短一致性越好,兜底价值仍在。- 大对象(详情页富文本、商品图列表)不要进本地缓存,会拖垮 GC。
二级缓存的一致性怎么保证
本地缓存的天然短板是“每个实例一份”,数据改了不会自动同步,两种主流做法:
- 广播失效,写入 Redis 后,通过 Redis Pub/Sub 或 MQ 发一条失效消息,各实例收到后删掉本地 key,优点是延迟低,缺点是引入了消息通道,要处理消息丢失。
- 短 TTL 自然过期,不广播,靠几秒后自动过期,实现简单,适合对一致性要求不极端的场景,比如商品标题、分类配置。
有个细节常被忽略:先更新数据库,再删缓存,最后广播失效,顺序反了,极端情况下会把旧值写回本地缓存,且要等 TTL 才自愈。
实操:读取链路怎么写
标准的读路径是“本地 → Redis → 数据库”,回填时加锁防击穿:
public String get(String key) {
String v = localCache.getIfPresent(key);
if (v != null) return v;
v = redis.opsForValue().get(key);
if (v != null) {
localCache.put(key, v);
return v;
}
// 未命中,加短锁重建,避免并发打库
String lockKey = "lock:" + key;
if (redis.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS)) {
try {
v = loadFromDb(key);
redis.opsForValue().set(key, v, 3600 + ThreadLocalRandom.current().nextInt(300), TimeUnit.SECONDS);
localCache.put(key, v);
} finally {
redis.delete(lockKey);
}
}
return v;
}
多实例部署还要注意两件事:本地缓存总内存等于单实例内存乘以副本数,集群规模大时要把 maximumSize 降下来;滚动发布时新实例是冷缓存,灰度阶段先放小流量。
中小团队做多级缓存改造大概要花多少钱
这块最容易被销售话术带偏,说几个实在的判断依据。
成本主要由四部分构成:
- 研发人力:接 Caffeine、改读写链路、加失效广播,按人月计,通常是小团队一到两个迭代周期的工作量。
- 机器成本:本地缓存吃的是应用本身的内存,机器规格可能要上调一档;Redis 基本不变。
- 可观测性:本地缓存命中率、Redis 命中率、数据库 QPS 三个指标必须进监控,这部分往往是隐性投入。
- 改造窗口:核心链路要灰度发布,需要预留回滚预案。
如果采购商业化的多级缓存或全链路压测服务,报价差异很大,建议按“是否必须自建”先做判断,绝大多数中小团队用开源组件加现成监控就能覆盖,钱主要花在人上,不是花在软件授权上。
上线前必须验证的几件事
- 断 Redis 演练,手动停掉 Redis,观察本地缓存是否接管、限流是否生效、数据库 QPS 峰值是多少。
- 压测对比,压测时看 P99 延迟和连接池活跃数,别只看平均响应时间。
- 监控看板,至少包含:Redis 命中率、本地缓存命中率、数据库 QPS、接口错误率、熔断触发次数。
- 告警阈值,命中率跌破某个水位立刻告警,不要等数据库扛不住了才发现。
一条可直接执行的命令,用来观察 Redis 延迟是否异常:
redis-cli --latency -h 127.0.0.1 -p 6379
关于缓存雪崩预防与本地缓存兜底的常见疑问
Q1:本地缓存会不会影响应用启动和内存占用?
会,但可控,启动时不要做全量预热,改成后台异步加载;内存方面用 maximumSize 硬性封顶,配合 GC 日志观察,通常几百 MB 的量级对现代 JVM 是可以接受的。
Q2:雪崩发生时直接重启 Redis 有用吗?
基本没用,甚至更糟,重启后缓存是空的,等于把所有流量一次性转给数据库,正确顺序是:先限流降级挡住流量,再恢复缓存,最后逐步放量。
Q3:本地缓存和 Redis 数据不一致时,排查路径是什么?
按“写入链路是否广播 → 消息是否消费成功 → TTL 是否配置生效”三步走,实践中多数不一致来自广播消息丢失或消费端异常,本地缓存本身极少出问题;广播不可靠时,把 expireAfterWrite 压到数秒,不一致窗口自然收窄到可接受范围。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/773504.html


