跳到内容

19.2 击穿、穿透、雪崩与缓存运维

缓存事故通常不是 Redis 单点变慢,而是缓存把源站隐藏的容量缺口突然暴露出来。设计目标不只是高命中率,还要确保 miss 被限制、合并和降级。

击穿:一个热点同时回源

热点 key 到期或被逐出后,大量并发请求同时发现 miss,全部查询源站。这也叫 cache stampede。

单进程请求合并

同一 key 同时只允许一个 loader 执行,其他请求等待其结果:

text
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% 命中率,也可能掩盖错误缓存导致的“高命中”。指标要和业务正确性一起看。

故障演练

在预生产或受控环境验证:

  1. 清空一个 namespace,观察预热和回源峰值;
  2. 断开部分应用到缓存的网络,确认超时与线程池隔离;
  3. 让热门 key 同时过期,验证 singleflight;
  4. 注入大量不存在 ID,验证限流与负缓存;
  5. 模拟旧失效事件晚到,确认 source version 不倒退;
  6. 缓存完全不可用时,确认降级功能而不是连锁拖垮数据库。

参考

下一章进入搜索索引。它和缓存一样是派生视图,但同步延迟、相关性和索引重建带来了另一组正确性问题。

Built with VitePress | Software Systems Atlas