Skip to content

缓存穿透、击穿、雪崩与一致性 ​

学习目标 ​

区分缓存失效的负载形态,为回源、重建和失效补偿设置可执行边界。

核心知识点 ​

本篇采用 Redis 8 稳定主版本的基础语义;官方文档核对日期:2026-10-06,不声明未验证的补丁版本。需要更高小版本的选项单独标明。

缓存是权威数据库数据的副本。穿透、击穿、雪崩关注回源负载,一致性关注读到的版本;解决负载并不自动解决一致性。统计命中率之外,还应观察回源 QPS/延迟、重建排队、锁等待、失效补偿积压和陈旧值窗口。

实验只在本机独立可丢弃 Redis 8、数据库 0 中执行。先通过终端运行 redis-cli -h 127.0.0.1 -p 6379,在交互提示符输入 redis 代码块;有 ACL 时使用 --user lab_user --askpass,不在命令中写密码。输出数组的排版因客户端而异。每节独立重建输入,命名空间 lab:{reliability}: 专用于学习;开始和结束只清理本节明确列出的 key,不把通配符传给删除命令。运行前确认没有业务数据,配置与恢复步骤另按注明的受控前提执行。

常用用法 ​

SET:空值缓存抑制缓存穿透 ​

用于缓存数据库明确不存在的结果并设置较短失效期。

redis
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:absent
SET lab:{reliability}:absent "{\"kind\":\"not_found\"}" EX 30
GET lab:{reliability}:absent
# 初始:数据库已确认这个业务 ID 不存在,key 只用于模拟该结果。
# 关键变化:absent 从缺失变为 kind=not_found 标记。
# 输出:清理数、OK、含 kind=not_found 的 JSON 字符串。

穿透是请求大量不存在的数据绕过缓存到数据库。标记必须与合法空字符串/空对象区分;缓存 miss 才查询数据库,命中 not_found 就返回不存在。设置短 TTL、校验 ID、限流,并在对象创建后失效负缓存;不能把数据库超时当作不存在,否则隐藏真实数据。攻击者制造海量唯一 ID 仍可能填满缓存。SET EX 把值和 TTL 放在一个原子命令中,避免 SET 后 EXPIRE 的断线窗口。SET。

BF.RESERVE:Bloom 过滤器与误判 ​

用于预先配置布隆过滤器后判断业务 ID 是否可能存在。

redis
# 初始:Redis 8 实例需实际提供 BF 命令,lab bloom key 不存在。
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:bloom
BF.RESERVE lab:{reliability}:bloom 0.01 1000
BF.ADD lab:{reliability}:bloom item-1
BF.EXISTS lab:{reliability}:bloom item-1
BF.EXISTS lab:{reliability}:bloom item-2
# 关键变化:item-1 对应位被加入,查询只能给出概率成员结论。
# 输出:清理数、OK、1、1;item-2 通常为 0,也可能因误判为 1。

部署必须验证 BF 命令可用,旧版纯 Redis 核心部署通常需 RedisBloom,不能假定每个服务都启用。0.01 是规划误判率,1000 是初始容量;扩展、超容量和多子过滤器影响成本。1 只表示可能存在,仍查缓存/数据库;0 只有在全量数据已同步且新增流程可靠时才能用于拒绝查询。数据装载遗漏、更新延迟造成的业务漏判不能归罪于算法;经典 Bloom 不支持直接删除单元素,需重建或另选结构。初始化、创建对象与过滤器更新之间需可靠事件或协调,不让缺失过滤器把所有有效 ID 拒绝。BF.RESERVE、BF.ADD、BF.EXISTS。

BF.ADD:添加业务 ID 并观察重复输入 ​

用于把一个已确认存在的业务 ID 加入布隆过滤器。

redis
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:bloom
BF.RESERVE lab:{reliability}:bloom 0.01 1000
BF.ADD lab:{reliability}:bloom item-1
BF.ADD lab:{reliability}:bloom item-1
# 初始:过滤器容量 1000,目标误判率 0.01,item-1 首次加入。
# 关键变化:item-1 首次加入改变过滤器,重复输入不再改变成员判断。
# 输出:清理数、OK、1、0。

复杂度取决于哈希函数数量;返回 0 是“可能已存在”,不是严格的数据库去重证明。不能用它决定账单是否已经处理。BF.ADD。

BF.EXISTS:查询可能成员而非精确业务状态 ​

用于观察同一个已加入 ID 的概率成员查询结果。

redis
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:bloom
BF.RESERVE lab:{reliability}:bloom 0.01 1000
BF.ADD lab:{reliability}:bloom item-1
BF.EXISTS lab:{reliability}:bloom item-1
# 初始:过滤器由本段重新创建,只有 item-1 已加入。
# 关键变化:插入后读取 item-1 的可能成员结论。
# 输出:清理数、OK、1、1。

复杂度取决于哈希函数数量与子过滤器数量;返回 1 不保证成员存在于权威数据库。查询前检查过滤器健康状态和同步进度,不把丢失过滤器返回的 0 当作权威拒绝。BF.EXISTS。

SET NX:互斥重建限制缓存击穿 ​

用于让同一热点 key 的并发 miss 只有一个请求获得重建资格。

redis
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:rebuild-lock lab:{reliability}:hot
GET lab:{reliability}:hot
SET lab:{reliability}:rebuild-lock owner-A NX PX 5000
SET lab:{reliability}:rebuild-lock owner-B NX PX 5000
GET lab:{reliability}:hot
# 初始:hot 不存在,两请求使用不同且实际应唯一的所有者 token。
# 关键变化:首次读取 miss;A 获得租约、B 失败,A 获锁后再次读取仍为 miss。
# 输出:清理数、nil、OK、nil、nil。

应用流程(模拟数据库读取,不是 Redis 命令):本例 A 的二次 GET 返回 nil,应用此时才读取权威数据库;为独立复核,假定读取成功得到字符串 db-version-7,再把这个结果传给下面的回填命令。若二次 GET 命中已有值,应用直接使用该值并跳过数据库读取和回填;数据库读取超时或失败也不能执行下面的 SET。这个模拟值不证明已连接或查询真实数据库。

redis
# 初始:仅 A 在二次 GET 为 nil 且模拟数据库读取成功后执行本段。
SET lab:{reliability}:hot "db-version-7" EX 60
GET lab:{reliability}:hot
# 关键变化:hot 从缺失变为模拟数据库结果 db-version-7,并带 60 秒 TTL。
# 输出:OK、"db-version-7"。

这是流程演示,命令逐个输入不模拟真实并发。击穿是热点过期导致同时回源;获得锁后必须二次查缓存,未获锁者有界等待加 jitter、读允许的旧值或降级,不能无限自旋。租约会过期,暂停的 A 可能与新 owner 并行,互斥不能证明缓存回填新鲜;比较版本/带 fencing 的写入协议另设计。数据库查询与回填限制时长,续租也有失败边界。释放必须原子比较 token 后删除,不直接 DEL 他人的锁;到期自然释放可演示恢复但增加等待。完整租约与 fencing 协议见本章后续锁专题。业务写入和重试仍需幂等。SET NX/PX、官方锁模式及限制。

SET EX:jitter 与缓存雪崩的负载边界 ​

用于给两个对象设置不同的过期时间以分散同时回源。

redis
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:jitter-a lab:{reliability}:jitter-b
SET lab:{reliability}:jitter-a a EX 61
SET lab:{reliability}:jitter-b b EX 74
TTL lab:{reliability}:jitter-a
TTL lab:{reliability}:jitter-b
# 初始:应用为基准 60 秒分别抽到 jitter 1 和 14 秒。
# 关键变化:jitter-a/jitter-b 被分配 61/74 秒到期时间。
# 输出:清理数、OK、OK;即时 TTL 接近 61 和 74,耗时后递减。

雪崩是大量缓存失效或 Redis 故障让回源激增。jitter 应在应用中随机抽取且有上下限;这里固定值只用于复核,生产不能所有实例取相同随机种子。TTL 随机化不解决 Redis 整体不可用,需回源并发预算、熔断、超时、限流、容量冗余、分批预热。逻辑过期方案可返回旧值并后台刷新,但必须业务允许陈旧、限制旧值最长期限并监控刷新失败;不可用于必须即时准确的余额等场景。SET EX。

DEL:Cache Aside 与延迟双删的一致性限制 ​

用于在明确数据库提交之后失效缓存并验证下次读取 miss。

redis
SET lab:{reliability}:cache v1
# 初始:数据库原值 v1;现在业务事务已成功提交 v2,Redis 仅模拟缓存。
# 危险:只删除这个可丢弃学习 key,生产必须确认业务对象对应名称。
# 边界:DEL 仅用于明确的可丢弃 lab 学习键范围,禁止生产批量误删。
DEL lab:{reliability}:cache
GET lab:{reliability}:cache
# 关键变化:已提交数据库 v2 后使缓存 v1 失效。
# 输出:OK、1、nil;后续 miss 从数据库读 v2 并回填。

Cache Aside 基本流程是读 miss 回源,写数据库成功后删缓存;这是应用协议,不是 Redis 与数据库的跨系统事务。失败删除需可靠 outbox/CDC/补偿队列、幂等消费、退避与告警,否则 TTL 前可陈旧。数据库提交前删除时,读者可能读取旧数据库并回填;先删、写库、延迟再删的“双删”可以降低某些窗口,但延迟无法覆盖任意暂停、慢读、重试或故障,因此不保证强一致性。即使提交后删除,早先读到旧数据的请求也可能随后回填;用版本条件回填、可靠失效与陈旧窗口预算缓解。必须强一致的读走权威数据库或设计统一的事务协议。Redis 原子删除只保证自身 key 操作;不要由 DEL 返回 1 推断数据库与缓存同时提交。DEL、Redis 事务范围。以上缓存竞态结论是基于两个独立存储与所示执行顺序的应用层推导。

易混点 ​

Bloom 的可能存在不是确定存在,互斥重建与 TTL jitter 不提供跨存储一致性。

课后小问 ​

  1. 双删等待一个足够长的固定时间是否保证一致?

答案:不能保证所有请求耗时有固定上限,重试和故障也会改变次序;需可靠补偿、版本协议或权威读取。

  1. 怎样处理写命令响应丢失?

答案:响应丢失不能证明没有执行;先检查业务状态,对非幂等写入使用业务 ID、去重记录或有界恢复流程。

本节小结 ​

把回源压力和版本正确性分别验证,业务允许的陈旧窗口决定缓存设计。

快速回顾 ​

按 H3 定位真实命令或操作流程,重建初始状态并读回结果,再检查超时、容量和数据安全边界。