Redis 缓存异常治理:穿透、热点失效与雪崩的识别和防护

缓存问题的共同风险不是“Redis miss”本身,而是大量请求在短时间内绕过缓存,把数据库或下游服务推过承载边界。解决问题前,应先识别回源范围、数据时效要求和可接受的降级行为。

本文以常见的 Cache-Aside 读路径为例,说明缓存穿透、热点 Key 失效和缓存雪崩如何区分、如何防护,以及发生故障时怎样保护回源链路。

1. 一次缓存读取如何回源

典型读路径如下:

  1. 请求先读取 Redis。
  2. 命中则直接返回。
  3. 未命中时查询数据库或下游服务。
  4. 查询成功后回填缓存,再返回结果。

这个路径需要三个基础约束:缓存读写必须有超时;回源必须有并发和速率上限;缓存内容的时效性必须由业务明确决定。否则,任何一种缓存失效都可能被放大成下游故障。

2. 三类问题如何快速区分

问题 触发条件 典型信号 回源范围
缓存穿透 请求的数据本不存在,缓存和数据库都未命中 不存在 Key 比例高、数据库空结果查询激增 持续的无效回源
热点失效(常称缓存击穿或 Cache Stampede) 单个或少数热点 Key 过期,很多请求同时重建 单 Key QPS 极高、该 Key miss 后数据库并发突增 局部热点回源
缓存雪崩 大量 Key 同时失效,或 Redis 整体不可用 缓存命中率骤降、Redis 错误率上升、多个库表同时承压 大面积回源

三者可能同时出现。例如 Redis 故障会放大热点 Key 回源,也可能让原本的无效请求直接打到数据库。因此方案不能只看名词,还要看回源是否可控。

3. 共同前提:Cache-Aside 与回源预算

Cache-Aside 的重点不是“查库后写 Redis”,而是让缓存始终可被丢弃和重建。写路径通常以数据库为准,再删除或失效缓存;不要把 Redis 当作唯一事实来源。

在设计每个缓存前,先确定:

  • 数据是否允许返回旧值,以及最长允许旧多久。
  • 单个 Key 的最大访问量和缓存重建成本。
  • Redis 不可用时,哪些请求可降级、哪些请求必须失败。
  • 数据库在缓存完全失效时能承受多少回源请求。

这些答案决定 TTL、预热、锁、限流和降级策略。没有回源预算时,随机 TTL 或分布式锁都只是局部补丁。

4. 缓存穿透:拦截无效请求

4.1 先区分业务错误与攻击

穿透指查询目标根本不存在,导致缓存 miss 后数据库也返回空。常见原因包括无效参数、过期链接、爬虫遍历 ID 和恶意构造随机 Key。

先观察空结果查询比例、来源分布、参数范围和单位时间的唯一 Key 数。若随机 Key 数持续上升,仅缓存空值会让 Redis 被无效键占满,必须在入口限制请求。

4.2 防护应是一条链

  1. 参数校验与鉴权:尽早拒绝非法格式、越界 ID 和无权限访问。
  2. 限流与风控:按用户、IP、接口或 Key 前缀控制突发请求,避免随机扫描直接进入回源链路。
  3. 空值缓存:数据库确认不存在后写入业务约定的空标识,并使用较短且带抖动的 TTL。
  4. 布隆过滤器:对相对稳定、规模大的合法 ID 集合,可在缓存前快速排除确定不存在的 Key。

空值缓存必须使用可区分于 Redis miss 的值,例如带版本的响应包装对象,而不是依赖空字符串等容易混淆的约定。数据创建、恢复或删除时也要考虑相关空值缓存的失效。

4.3 布隆过滤器的边界

布隆过滤器的结论是:判断“不存在”时一定不存在;判断“可能存在”时仍需继续查缓存或数据库。它会有假阳性,不会有假阴性。

使用前需要估算元素数量、可接受假阳性率和内存预算,并设计新增、删除、重建和版本切换流程。它用于降低无效回源,不是权限校验,也不能替代限流。

5. 热点 Key 失效:控制重建并发

热点失效的本质是“同一个昂贵回源操作被并发执行”。判断时应同时看该 Key 的访问量、过期时间、数据库查询耗时和重建期间的回源并发数。

5.1 方案一:请求合并或互斥重建

适用于读取必须尽快得到新值的场景:

  1. 读取缓存,命中即返回。
  2. miss 后尝试取得该 Key 的短租约重建权限。
  3. 成功者查询数据源并回填缓存。
  4. 未成功者短暂退避后重新读取缓存;达到重试上限则按业务策略失败或降级,不能无限自旋。

锁必须带唯一令牌和过期时间,释放时应原子校验令牌,避免旧持有者误删新锁。重建超过租约时应有续租或超时处理;更重要的是为每个 Key 和整个数据源设置回源并发上限。

同一进程内可先使用 singleflight 一类请求合并机制,再决定是否需要跨实例分布式协调。不要为每一次普通 miss 都增加昂贵的分布式锁。

5.2 方案二:逻辑过期与后台刷新

适用于允许短暂返回旧值的热点数据。缓存值包含逻辑过期时间;逻辑过期后,只有一个受控的后台任务刷新数据,其余请求继续读取旧值。

这种模式本质是 stale-while-revalidate:它提升可用性,但会牺牲一段时间的一致性。因此应明确最大陈旧窗口、刷新失败后的退避策略和删除数据的处理方式。物理 TTL 不应无限期保留而没有兜底清理机制。

5.3 热点预热与过期设计

可预见的热点,如活动商品或首页配置,应在流量到来前预热,并为过期时间增加适度抖动。预热不是替代回源保护;仍要为首次加载、主动失效和刷新失败准备限流与降级。

6. 缓存雪崩与 Redis 故障:扩大防线

6.1 打散过期时间

同一批 Key 使用相同 TTL 会形成集中回源。可将过期时间设置为“基础 TTL + 有界随机抖动”,并避免在系统启动时一次性写入全部缓存。

这只能缓解批量过期,不能解决 Redis 自身不可用、网络分区、连接池耗尽或业务流量突增。

6.2 Redis 高可用不是回源保护

复制、哨兵或集群可以降低单节点故障概率,但故障切换、热点倾斜、客户端超时和错误配置仍会造成缓存不可用。应用侧必须设置短超时、有限重试和连接池隔离,避免请求长期挂起后一起回源。

6.3 Redis 不可用时的降级顺序

建议按业务优先级设计,而不是无差别回源:

  1. 对可缓存的公共读请求,可使用容量受控、TTL 较短的本地缓存兜底。
  2. 对非核心功能,返回默认数据、稍后重试或直接降级。
  3. 对必须访问数据源的核心请求,使用全局限流、舱壁隔离和数据库连接保护。
  4. 当下游延迟和错误率持续恶化时,熔断非必要调用,保留恢复空间。

本地缓存会带来多实例不一致和内存膨胀风险,必须限定容量、TTL 和失效传播策略。不要把多级缓存当作无需维护的“万能兜底”。

7. 方案选择表

场景 首选方案 主要代价
高频查询不存在的数据 参数校验 + 限流 + 空值缓存 空值占用内存,需处理创建后的失效
合法 ID 集合稳定且规模大 布隆过滤器 + 空值缓存 需要维护重建与假阳性率
热点数据必须尽快读到新值 请求合并/互斥重建 + 有界退避 等待、锁租约和回源上限需要精心设计
热点数据可短暂陈旧 逻辑过期 + 后台刷新 需要接受陈旧读并处理刷新失败
批量 Key 集中过期 TTL 抖动 + 分批预热 不能覆盖 Redis 故障
Redis 故障 高可用 + 本地缓存 + 限流降级 一致性下降,系统复杂度提高

8. 监控、压测与演练

至少持续观测:缓存命中率、按 Key 前缀划分的 miss 率、空值命中数、热点 Key QPS、回源 QPS、Redis 延迟/错误率、数据库连接与慢查询、锁等待和异步刷新队列长度。

上线前应验证三个故障场景:随机无效 Key 洪泛、热点 Key 同时过期,以及 Redis 部分或全部不可用。验收标准应是数据库和下游服务仍在回源预算内,而不是“Redis 没有报错”。

9. 上线检查清单

  • 已定义该缓存允许的陈旧时间与 Redis 故障时的降级行为。
  • 已为回源设置超时、并发上限和限流策略。
  • 已区分空值缓存、Redis miss 与真实业务空响应。
  • 热点重建不会无限重试或无界创建后台任务。
  • TTL、预热与主动失效不会制造集中回源。
  • 已通过压测或演练验证 Redis 故障时的下游保护。