学习日报|梳理三类典型缓存问题:缓存穿透、缓存击穿、缓存雪崩
一、缓存穿透(Cache Penetration)
What:是什么
请求的 key 在缓存与数据库都不存在,导致每次都 绕过缓存直打数据库;在高并发或恶意流量下,DB 被打穿。
Why:为什么会发生
恶意构造随机/不存在的 ID。
业务参数校验缺失(负数/超范围 ID)。
刚上线/冷数据天然不存在。
When:典型触发时机
新功能放量、促销活动、黑产扫描接口。
分布式爬虫对详情页/查询接口“打点式”扫描。
Scenario:表现/定位线索
缓存命中率下降,但 不存在的 key 占比高。
DB QPS、慢查询上升;热点并不集中,而是“均匀稀疏”的随机 key。
应用日志大量 404/空结果。
How:治理与防护
1)快速止血
接口参数校验前置:ID 边界/白名单校验,不合法直接拦截。
缓存空值(短 TTL + 随机抖动):不存在也写缓存
TTL:30s~2min(按业务容忍度);并加 0~10s 随机抖动防止齐过期。
缺点:占用少量缓存空间;可配“空值计数”+LRU 限制。
网关限速/验证码:对异常维度限流(例如 UA/IP/Referer)。
2)中长期治理
布隆过滤器(Bloom Filter):挡掉“必然不存在”的 key
适合“ID 是否存在”判定;误判率可控(如 1e-4)。
维护方式:启动全量构建 + 增量同步(binlog/MQ)。
RedisBloom 用法(示例):
BF.RESERVE item_bf 0.0001 50000000 BF.ADD item_bf 123 BF.EXISTS item_bf 456 // false -> 直接返回空或拦截
多级缓存:本地(Caffeine/Guava)+ 分布式(Redis),先查本地能进一步抵御穿透。
安全策略:WAF、机器人规则、黑名单、频次惩罚。
示例伪代码(Java/Caffeine + Redis + 空值缓存)
String cacheKey = "item:" + id;
String local = localCache.getIfPresent(cacheKey);
if (local != null) return decode(local);if (!bloom.exists(id)) return Result.empty(); // 快速挡掉String redisVal = redis.get(cacheKey);
if (redisVal != null) return decode(redisVal);// DB 查询
Item item = repo.findById(id);
if (item == null) {redis.setex(cacheKey, randTtl(30, 10), NULL_MARKER);return Result.empty();
}
redis.setex(cacheKey, randTtl(300, 60), encode(item));
localCache.put(cacheKey, encode(item));
return item;
二、缓存击穿(Cache Breakdown / Stampede)
What:是什么
单个热点 key 过期或被动失效 的瞬间,大量并发 同时未命中,全部回源 DB,造成 尖刺式 流量冲击。
Why:为什么会发生
热点数据 TTL 到点统一过期。
大 key 被误删/手动失效。
热点在短时暴涨(突发热点)。
When:典型触发时机
秒杀/大促、热门内容突然爆火。
定点 00:00/整点批量过期。
Scenario:表现/定位线索
某少量 key 的 miss 突增;DB 后端出现短时高峰。
接口 P99 延迟峰值在热点 key 上聚集。
Redis QPS 正常但应用到 DB 的回源强烈“尖峰”。
How:治理与防护
1)请求合并/互斥(SingleFlight)
同一 key 的并发 miss,只放行 一个请求回源,其它等待,回源结果落缓存后“羊群分食”。
可用分布式锁(SETNX + EX)或进程内 singleflight(如 Go sync/singleflight、Java 本地锁)。
2)逻辑过期 + 异步重建
缓存值内带
expireAt
(逻辑过期);过期后仍返回旧值,同时异步触发刷新,避免“黑洞期”。适合读多写少且可接受短时旧数据的场景。
3)永不过期(物理)+ 后台定时刷新
Redis 层不设 TTL 或较长 TTL;定时器/消息驱动刷新热点 key。
配合“版本号/etag”防脏读。
4)多级缓存 + 热 key 保护
本地缓存兜底;Nginx/Sidecar 层可做 microcache(1~3s)抗瞬时击穿。
一些网关/代理支持热 key 合并(request collapsing)。
5)提前续租(Expire-After-Access + 提前刷新)
在 TTL 进入“刷新窗口”时(例如剩余 <20%),后台线程提前刷新。
示例伪代码(分布式锁 + 双层缓存)
String cacheKey = "hot:" + id;
String v = redis.get(cacheKey);
if (v != null) return decode(v);// 只允许一个线程回源
String lockKey = "lock:" + cacheKey;
if (redis.set(lockKey, "1", "NX", "EX", 5)) {try {Item x = repo.findById(id);if (x == null) {redis.setex(cacheKey, randTtl(30, 10), NULL_MARKER);return Result.empty();}redis.setex(cacheKey, randTtl(300, 60), encode(x));localCache.put(cacheKey, encode(x));return x;} finally {redis.del(lockKey);}
} else {// 失败则小睡 + 读本地缓存兜底sleep(30);String local = localCache.getIfPresent(cacheKey);if (local != null) return decode(local);return Result.tryLater(); // 或短路降级
}
三、缓存雪崩(Cache Avalanche)
What:是什么
大量 key 在同一时刻集中失效(或缓存集群整体不可用),导致 大面积回源,DB 被洪峰压垮。
Why:为什么会发生
批量设置了相同 TTL(齐过期)。
集群整体故障/网络隔离/主从同时抖动。
集中式预热后未做随机抖动。
When:典型触发时机
整点统一过期策略、批量回灌的固定时间点。
容量变更/切集群/故障恢复后的首次大流量。
Scenario:表现/定位线索
缓存命中率“悬崖式”下跌;DB/下游服务同时暴涨。
Redis 集群节点抖动、连接暴增、超时上升。
网关层出现全链路拥塞(队列积压、限流触发)。
How:治理与防护
1)TTL 随机抖动(核心)
为每个 key 的 TTL 加
±(0~10%)
随机,避免同刻过期。
2)分层缓存 & 微缓存
本地缓存(Caffeine)兜底 1~5s;Nginx/Sidecar 微缓存 1~3s 抗瞬时洪峰。
3)流量治理
限流:令牌桶/漏桶,优先保护下游(DB/SaaS)。
熔断/降级:不可用时快速失败或返回兜底数据(骨架屏、上次成功快照)。
舱壁隔离:线程池/连接池隔离不同接口,避免一处雪崩拖垮全站。
4)高可用架构
Redis 集群 + 副本 + 多 AZ/多机房;哨兵/Proxy 自动切换。
双集群/双活 或“读多活写单元化”;关键热点维持 只读副本缓存。
5)预热与分批回灌
上线/扩容后按 批次 预热,错峰写入缓存。
热点清单(TopN)优先级回灌;异步后台校准。
四、三者差异一图懂
维度 | 穿透 | 击穿 | 雪崩 |
---|---|---|---|
命中对象 | 大量不存在的 key | 单个/少量热点 key 失效 | 大量 key 同时失效/集群故障 |
流量形态 | 稀疏、随机 | 尖刺集中 | 洪峰、面状 |
快速止血 | 参数校验/空值缓存/布隆/限速 | 请求合并/逻辑过期/本地兜底 | TTL 抖动/限流熔断/多级缓存 |
中长期 | 布隆+增量维护 | 永不过期+后台刷新 | 高可用 + 错峰回灌 |
五、落地清单(Checklist)
通用
TTL 统一加随机抖动(±10% 参考)。
本地缓存 + 分布式缓存 二级架构。
接口参数严格校验;网关维度限流。
关键接口舱壁隔离 + 熔断降级策略。
关键路径埋点与追踪(命中/回源/DB)。
穿透专属
RedisBloom/Guava Bloom:全量构建 + binlog/MQ 增量。
空值缓存(短 TTL);黑白名单 & WAF。
击穿专属
SingleFlight/SETNX 互斥锁。
逻辑过期 + 异步重建;提前刷新窗口。
热点 Key 清单与保护策略(永不过期/后台刷新)。
雪崩专属
错峰预热与分批回灌。
多活/跨 AZ;微缓存兜底。
全链路限流 → 熔断 → 降级的三级闸门。
六、监控与排查(SRE 视角)
核心指标
命中率(总体 & 接口 & key 维度)。
回源率(Miss→DB 比例)、DB QPS/延迟/慢查询。
热点分布(Top N key、热点迁移)。
Redis:连接数、ops、内存、淘汰、阻塞、复制延迟。
应用:线程池队列长度、P95/P99、错误率、超时率。
网关:限流命中、熔断次数、微缓存命中。
告警规则
命中率骤降阈值(如 15%+ 在 1 分钟内)。
单 key 回源速率/请求并发异常突增。
DB 连接池耗尽/慢查数激增。
Redis 主从复制落后/不可达。
七、写策略与一致性(触类旁通)
Cache Aside(旁路缓存):读先查缓存,miss 再 DB → 回写缓存;写:先 DB 再删缓存(延迟双删 10~500ms,抗读写并发脏读)。
Write-Through:写 DB 同时写缓存(代理层实现)。
Write-Behind:写入先落缓存/队列,异步冲刷 DB(需幂等/丢失可接受)。
消息驱动失效:binlog/MQ 广播精确失效,降低 TTL 依赖。
延迟双删示例
updateDB(id, data);
redis.del(key(id));
sleep(100); // 视业务与链路时延调整
redis.del(key(id)); // 再删一次,降低并发脏读窗口
八、方案选型建议(速查)
读多写少 & 可容忍轻微旧数据:逻辑过期 + 异步刷新 + 本地缓存。
强一致读:singleflight + 短 TTL + 精确失效(MQ/binlog),必要时读 DB 直出。
超大流量 & 热点不可丢:永不过期 + 后台刷新 + Nginx 微缓存 + 限流熔断。
安全风险高:布隆 + 空值缓存 + 严限流 + 验证码/WAF。
如果你愿意,我可以把这份指南整理成可复用的“团队维基页面”或加上你的技术栈(比如 Spring + Redisson、Go、Node)给出更贴近生产的示例代码与配置。