2026年7月13日 · 6 分钟阅读

缓存雪崩、穿透、击穿与数据库一致性:从故障到治理

区分缓存雪崩、穿透和热点击穿,系统梳理 TTL 抖动、布隆过滤器、互斥重建、限流降级,以及 Cache-Aside 与数据库一致性的工程方案。

缓存把数据库从高并发读取中保护起来,也可能在失效时把流量一次性还给数据库。雪崩、穿透和击穿表面上都是“请求落到数据库”,根因和治理方式却不同。

三类问题先分清

问题根因流量形态典型治理
缓存雪崩大量 key 同时失效或 Redis 整体不可用大面积回源TTL 抖动、高可用、多级缓存、限流降级、预热
缓存穿透查询本来就不存在的数据不同无效 key 持续回源参数校验、空值缓存、Bloom Filter、风控限流
缓存击穿单个超热点 key 失效同一 key 瞬时并发回源single-flight、互斥重建、逻辑过期、提前刷新

缓存雪崩

雪崩有两种常见来源:批量写入的 key 使用相同 TTL,在同一时刻过期;或者 Redis 节点、网络或集群整体异常,所有请求突然未命中。

治理要分层:

  • TTL 加随机抖动,把集中失效摊开。
  • 活动前预热热点,但控制预热速率,避免预热本身压垮数据库。
  • 使用 Redis 高可用和合理的持久化恢复策略。
  • 对极少量稳定热点使用短时本地缓存或陈旧数据兜底。
  • 对数据库入口限流、熔断和降级,阻止故障级联。
  • 冷启动时逐步放量,不让所有实例同时回源。

高可用不能替代限流。故障转移也有时间窗口,数据库必须能拒绝超过安全容量的请求。

缓存穿透

攻击者不断查询不存在的用户 ID,每次 Redis 和数据库都查不到。如果无效 key 不重复,简单空值缓存无法完全解决。

按成本从低到高治理:

  1. 在入口校验 ID 范围、格式、权限和签名。
  2. 对确实不存在的数据缓存短 TTL 空值,并区分“空值”与缓存未命中。
  3. 使用 Bloom Filter 判断 key 是否可能存在。
  4. 对异常来源做限流、验证码或风控封禁。

Bloom Filter 的“确定不存在”可以阻止回源,但“可能存在”仍需查询;它存在假阳性,并要处理新增、删除和重建。不能把它描述成绝对准确的数据库索引。

缓存击穿

爆款商品 key 过期的瞬间,几千个线程同时查库并重建。解决思路是让同一 key 同一时刻只有一个回源任务:

请求未命中
  → 尝试获得短租约重建权
  → 获得者查库并写缓存
  → 其他请求短暂等待、返回旧值或降级结果

分布式互斥锁需要唯一持有者标识、原子校验删除和超时;数据库慢于锁租约时仍可能出现多个重建者。缓存重建通常更适合 single-flight 或带时间预算的短锁,不应把它包装成强一致分布式锁。

对允许短暂陈旧的数据,可以采用逻辑过期:value 中记录业务过期时间,过期后先返回旧值,再由一个线程异步刷新。它牺牲新鲜度换取稳定尾延迟。

Cache-Aside 的一致性窗口

常见更新顺序是“先更新数据库,再删除缓存”:

@Transactional
public void updateProduct(Product product) {
    productRepository.update(product);
    // 实际工程中应在事务提交后执行删除,避免事务回滚却删掉有效缓存
    afterCommit(() -> redis.del("cache:product:v2:" + product.id()));
}

为什么不先删缓存再更新数据库?

线程 A 删除缓存
线程 B 未命中,读取数据库旧值
线程 A 更新数据库
线程 B 把旧值写回缓存

先更新数据库再删缓存也不是原子操作:数据库成功而删除失败,会保留旧缓存;或者一个极慢的旧值回源在更新后才写缓存。工程上要用 TTL 限制最长陈旧时间,并通过重试、事件或 CDC 让删除最终成功。

一致性方案对比

方案做法优点风险与适用范围
更新 DB 后删缓存事务提交后失效 key简单、通用删除失败和并发回填形成短窗口
延迟双删先删,更新后再删,延迟后重删降低旧值回填概率延迟值难准确选择,不能证明强一致
MQ/CDC 失效订阅业务事件或 binlog 删除缓存与业务事务解耦,可重试审计有传播延迟,需要幂等和积压治理
Write-Through统一缓存层同步写数据源写路径集中增加延迟和系统耦合
Write-Behind缓存异步写数据库写吞吐高数据安全、顺序和恢复复杂

关键业务若必须读到最新值,可以在写后短时间绕过缓存、带版本号读取,或直接访问主库。不要用最终一致缓存承担强一致需求。

CDC 失效的工程流程

数据库 binlog 或 Outbox 产生变更事件,消费者根据实体 ID 删除对应 key:

数据库提交 → binlog / outbox → MQ → cache invalidator → DEL key

删除操作天然易于幂等,但仍需监控消息延迟、重试和死信。缓存 key 的映射必须可从变更事件确定;复杂聚合缓存可能依赖多个实体,需要维护反向索引或直接缩短 TTL。

故障保护与恢复

事故中按以下顺序处理:

  1. 判断是全局失效、无效请求还是单热点 key。
  2. 限制回源并发,保护数据库连接池和线程池。
  3. 对非关键功能返回默认值、旧值或静态页。
  4. 修复 Redis 或热点重建逻辑,分批预热。
  5. 观察命中率、回源 QPS、数据库延迟和错误率后逐步放量。
  6. 对账修复不一致数据,复盘 TTL 分布、告警和演练。

工程检查清单

  • TTL 是否按业务新鲜度设计并加入抖动。
  • 空值是否有独立标记和较短过期时间。
  • 热点 key 失效是否采用 single-flight、逻辑过期或提前刷新。
  • Redis 不可用时数据库入口是否有明确并发上限。
  • 缓存删除失败是否可重试、可观测、可审计。
  • 写后必须读新的场景是否绕过最终一致缓存。
  • 预热、重放和 CDC 消费是否受速率控制。

面试回答要点

回答时先用“范围、数据是否存在、是否热点”区分雪崩、穿透和击穿,再分别给方案。谈一致性时说明 Cache-Aside 常用“更新数据库后删缓存”,但它仍有并发和删除失败窗口,需要 TTL、重试或 CDC 收敛;如果业务要求强一致,应绕过缓存或采用更强的业务协议。

总结

缓存故障治理的目标不是让 Redis 永不失败,而是让失败不会击穿数据库。TTL 抖动处理集中失效,空值与 Bloom Filter 处理不存在数据,single-flight 和逻辑过期处理热点重建;一致性则通过明确事实来源、删除缓存、最终重试和业务新鲜度边界来控制。

参考资料