19.2 击穿、穿透、雪崩与缓存运维
缓存事故通常不是 Redis 单点变慢,而是缓存把源站隐藏的容量缺口突然暴露出来。设计目标不只是高命中率,还要确保 miss 被限制、合并和降级。
击穿:一个热点同时回源
热点 key 到期或被逐出后,大量并发请求同时发现 miss,全部查询源站。这也叫 cache stampede。
单进程请求合并
同一 key 同时只允许一个 loader 执行,其他请求等待其结果:
singleflight(key):
first caller -> load source -> fill cache -> wake waiters
followers -> await same result要限制等待者数量、总超时和取消传播;loader 失败时不能让所有等待者无限排队。
跨实例互斥
分布式锁可减少多实例同时回源,但必须带过期时间和唯一 owner token,释放时用原子 compare-and-delete,避免旧持有者删除新锁。锁租约可能在慢查询期间过期,所以源站写入还需版本检查或 fencing token;缓存重建锁不是业务正确性锁。
逻辑过期与 stale-while-revalidate
允许返回短暂旧值时,可在缓存值里保存逻辑过期时间:过期后一个请求刷新,其他请求继续读旧值。它用陈旧换可用,必须定义最大陈旧时长,不能用于撤权、余额等必须立即生效的数据。
穿透:请求的数据根本不存在
攻击或 bug 可能不断生成新无效 key,让每次请求都绕过缓存访问数据库。
防线按成本从外到内:
- API 鉴权、格式与范围校验;
- 限流和按租户配额;
- 对确认不存在的结果做短期负缓存;
- 已知有效 ID 集合可用 Bloom filter 做前置筛选;
- 监控 distinct miss key、404 比例与来源。
Bloom filter 有假阳性,没有假阴性只在集合维护正确且未发生错误删除时成立。它判定“可能存在”仍要查源;判定“一定不存在”才能挡回源。动态删除需要 counting Bloom filter 或重建策略。
负缓存不能把暂时数据库错误当作“不存在”,否则故障会被缓存成持续 404。
雪崩:大量 key 或整个缓存层同时失效
触发原因包括:
- 大批 key 使用相同绝对过期时间;
- Redis 集群故障、网络隔离或凭据过期;
- 发布改了 key namespace,瞬间全部冷启动;
- 淘汰策略和容量不足造成持续 churn;
- 热点迁移后新节点没有预热。
缓解措施需要组合:
- TTL 加与业务上界兼容的小幅 jitter;
- 按 key/租户做请求合并和回源并发上限;
- circuit breaker、队列和负载削减;
- 关键热点异步刷新与受控预热;
- 缓存故障时返回可接受旧值或功能降级;
- 源站按“缓存全失”的最小生存流量做容量演练。
随机 TTL 只能避免同批过期,解决不了集群整体不可用。
淘汰策略决定谁先离开,不保证命中
Redis 可按 allkeys/volatile 范围结合 LRU、LFU、random、TTL 等策略,也可选择 noeviction。LRU/LFU 是近似实现,实际效果取决于版本、采样、对象大小与访问分布。
选择前先区分:
- 所有 key 是否都可安全淘汰;
- 哪些 key 设置 TTL;
- 热点是否稳定或快速变化;
- 大 key 是否挤掉大量小热点;
- 达到上限时宁愿写失败还是淘汰旧缓存。
命中率下降可能是容量不足、TTL 太短、key 基数暴涨、请求模式改变或大 key 驱逐造成,不能只通过切换淘汰算法修复。
大 Key、热 Key 与序列化成本
大对象带来网络、复制、持久化、删除和反序列化尖峰。应监控 value 大小分布,按页/字段拆分有界对象,避免一个无限增长的 Hash/List/Set。
热 key 即使很小,也会把单 shard 的 CPU 或网络打满。可以:
- 本地短 TTL 复制只读热点;
- 将可交换计数分片后汇总;
- 让请求合并减少重复读取;
- 改数据模型分散 key,但接受读取 fan-out;
- 对无法拆分的强一致热点承认其串行上限。
缓存观测指标
至少分层、分业务观察:
- hit/miss rate 与 distinct miss keys;
- hit、miss、回源的 p50/p95/p99;
- evictions、expired keys 与 rejected writes;
- memory used、fragmentation、key/value 大小分布;
- hot key、单 shard QPS 与网络带宽;
- loader 并发、等待队列、超时和失败率;
- 缓存值与权威版本的抽样差异;
- 全失效后源站剩余容量。
全局 95% 命中率可能掩盖一个关键接口 20% 命中率,也可能掩盖错误缓存导致的“高命中”。指标要和业务正确性一起看。
故障演练
在预生产或受控环境验证:
- 清空一个 namespace,观察预热和回源峰值;
- 断开部分应用到缓存的网络,确认超时与线程池隔离;
- 让热门 key 同时过期,验证 singleflight;
- 注入大量不存在 ID,验证限流与负缓存;
- 模拟旧失效事件晚到,确认 source version 不倒退;
- 缓存完全不可用时,确认降级功能而不是连锁拖垮数据库。
参考
下一章进入搜索索引。它和缓存一样是派生视图,但同步延迟、相关性和索引重建带来了另一组正确性问题。