2. 缓存一致性基础:缓存穿透、缓存击穿、缓存雪崩、缓存与数据库双写不一致、最终一致性 vs 强一致性

2.1 缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存层和数据库层都无法命中。由于缓存层不存储空值,每次请求都会直接穿透到数据库,导致数据库压力剧增,甚至引发宕机。

典型场景:恶意攻击者伪造大量不存在的用户ID(如负数或超大整数)发起查询,缓存层全部失效,数据库被高频空查询打垮。

解决方案对比

方案 原理 优点 缺点
缓存空对象 将查询结果为 null 的 key 也写入缓存,设置较短 TTL 实现简单,通用性强 占用缓存空间,TTL 内数据不一致
布隆过滤器 在缓存前加一层 Bloom Filter,判断 key 是否存在 内存占用极低,不存在则直接拦截 存在误判率,不支持删除操作
参数校验与限流 对请求参数做合法性校验,配合令牌桶/漏桶限流 从入口层阻断恶意请求 无法防御合法但不存在的数据查询

2.2 缓存击穿

缓存击穿是指一个热点 key 在缓存过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库,导致数据库负载瞬间飙升。

关键特征:单点热点 key 失效 + 高并发读。与缓存穿透的区别在于:击穿针对的是存在的数据,只是缓存恰好过期。

常用解决方案

  • 互斥锁(Mutex Lock):缓存失效后,只允许一个线程去数据库加载数据,其他线程等待或轮询。适用于写少读多的场景。
  • 逻辑过期:缓存中不设置物理 TTL,而是存储一个逻辑过期时间字段。发现过期时,异步线程去更新缓存,当前线程直接返回旧数据。
  • 热点 key 永不过期:对极高热点的 key 不设置过期时间,通过后台定时任务主动更新。
缓存击穿流程与互斥锁方案 客户端并发请求 缓存层(Redis) 互斥锁(SETNX) 数据库(MySQL) 返回数据并回填缓存 1. 请求热点 key 2. key 过期 3. 加锁成功,查询 DB 4. 查询结果 5. 回填缓存 6. 返回数据 其他并发请求 未抢到锁,自旋等待 或直接返回旧数据 (取决于策略)

2.3 缓存雪崩

缓存雪崩是指大量缓存 key 在同一时间过期失效,或者缓存节点宕机,导致所有请求直接打到数据库,造成数据库瞬间过载甚至崩溃。

雪崩与击穿的区别:击穿是单个热点 key 失效,雪崩是大规模 key 同时失效或缓存层整体不可用。雪崩的破坏力远大于击穿。

预防与应对措施

  • 过期时间加随机偏移:在基础 TTL 上增加 ±10%~30% 的随机值,避免大量 key 同时过期。
  • 缓存集群高可用:采用 Redis Sentinel 或 Redis Cluster,避免单点故障。
  • 多级缓存:本地缓存(Caffeine/Guava)+ 分布式缓存(Redis)组合,本地缓存作为第一道屏障。
  • 限流降级:对数据库访问做熔断限流,保护后端服务。

2.4 缓存与数据库双写不一致

在读写并发场景下,缓存和数据库之间的数据更新顺序不一致,导致缓存中存储的是旧数据(脏数据),而数据库已更新为新数据。

常见更新策略对比

策略 操作顺序 不一致风险 适用场景
Cache Aside(旁路缓存) 读:先读缓存,miss 则读 DB 并回填
写:先更新 DB,再删除缓存
低(删除缓存而非更新,避免并发写覆盖) 读多写少,通用场景
Read/Write Through 缓存层代理读写,缓存负责同步 DB 中(依赖缓存中间件的一致性保证) 需要缓存层封装复杂逻辑
Write Behind(异步回写) 先写缓存,异步批量写 DB 高(宕机可能丢数据) 写吞吐极高,可接受短暂不一致
最佳实践:业界最推荐 Cache Aside + 延迟双删。先更新数据库,再删除缓存;延迟几百毫秒后再删除一次缓存,解决并发读请求在第一次删除后回填旧数据的问题。
Cache Aside + 延迟双删时序 写请求 数据库 缓存(Redis) 读请求 ① 更新 DB ② 删除缓存 延迟 500ms ③ 再次删除缓存 并发读 读缓存 miss 读 DB 回填旧数据 延迟双删原理 第二次删除用于清除并发读请求在第一次删除后回填的旧缓存数据,保证最终缓存中是最新数据。

2.5 最终一致性 vs 强一致性

在分布式缓存系统中,强一致性要求每次读操作都能读到最新的写入结果;最终一致性允许短暂的不一致窗口,但保证经过一段时间后所有副本最终达到一致状态。

核心对比

维度 强一致性 最终一致性
读延迟 高(需要同步等待所有副本确认) 低(直接读本地副本)
写可用性 低(部分副本故障则写入失败) 高(写入多数副本即可返回)
实现复杂度 高(需要 Paxos/Raft 等共识算法) 中(异步复制 + 冲突检测)
典型协议 ZAB、Raft、Paxos Gossip、Dynamo 风格
缓存场景适用性 金融交易、库存扣减 用户信息、商品详情、Feed 流
缓存一致性取舍:在缓存场景中,绝大多数业务选择最终一致性。因为缓存本身是性能加速层,如果为了强一致性引入分布式事务或共识协议,会大幅增加延迟,违背缓存设计的初衷。对于必须强一致的场景(如支付、库存),建议直接读数据库主库,或使用分布式锁+版本号机制。

最终一致性在缓存中的实现路径

  1. 异步 binlog 订阅:通过 Canal 监听 MySQL binlog,变更后异步删除/更新缓存。
  2. 消息队列通知:业务写 DB 后发送 MQ 消息,消费端更新缓存。
  3. 版本号/时间戳:缓存中存储数据版本号,读时对比版本,旧版本触发异步刷新。
  4. TTL 兜底:设置合理的过期时间,即使缓存不一致,也会在 TTL 后自动修复。
可观测性视角:在最终一致性方案中,不一致窗口的监控至关重要。通过 OpenTelemetry 追踪每次缓存更新操作的链路,记录“DB 更新时间”与“缓存更新时间”的差值,可以量化不一致窗口长度,并在窗口超过阈值时触发告警。这正是本课程后续章节的核心实践。