2. 缓存一致性基础:缓存穿透、缓存击穿、缓存雪崩、缓存与数据库双写不一致、最终一致性 vs 强一致性
2.1 缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和数据库层都无法命中。由于缓存层不存储空值,每次请求都会直接穿透到数据库,导致数据库压力剧增,甚至引发宕机。
典型场景:恶意攻击者伪造大量不存在的用户ID(如负数或超大整数)发起查询,缓存层全部失效,数据库被高频空查询打垮。
解决方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 缓存空对象 | 将查询结果为 null 的 key 也写入缓存,设置较短 TTL | 实现简单,通用性强 | 占用缓存空间,TTL 内数据不一致 |
| 布隆过滤器 | 在缓存前加一层 Bloom Filter,判断 key 是否存在 | 内存占用极低,不存在则直接拦截 | 存在误判率,不支持删除操作 |
| 参数校验与限流 | 对请求参数做合法性校验,配合令牌桶/漏桶限流 | 从入口层阻断恶意请求 | 无法防御合法但不存在的数据查询 |
2.2 缓存击穿
缓存击穿是指一个热点 key 在缓存过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库,导致数据库负载瞬间飙升。
关键特征:单点热点 key 失效 + 高并发读。与缓存穿透的区别在于:击穿针对的是存在的数据,只是缓存恰好过期。
常用解决方案
- 互斥锁(Mutex Lock):缓存失效后,只允许一个线程去数据库加载数据,其他线程等待或轮询。适用于写少读多的场景。
- 逻辑过期:缓存中不设置物理 TTL,而是存储一个逻辑过期时间字段。发现过期时,异步线程去更新缓存,当前线程直接返回旧数据。
- 热点 key 永不过期:对极高热点的 key 不设置过期时间,通过后台定时任务主动更新。
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 + 延迟双删。先更新数据库,再删除缓存;延迟几百毫秒后再删除一次缓存,解决并发读请求在第一次删除后回填旧数据的问题。
2.5 最终一致性 vs 强一致性
在分布式缓存系统中,强一致性要求每次读操作都能读到最新的写入结果;最终一致性允许短暂的不一致窗口,但保证经过一段时间后所有副本最终达到一致状态。
核心对比
| 维度 | 强一致性 | 最终一致性 |
|---|---|---|
| 读延迟 | 高(需要同步等待所有副本确认) | 低(直接读本地副本) |
| 写可用性 | 低(部分副本故障则写入失败) | 高(写入多数副本即可返回) |
| 实现复杂度 | 高(需要 Paxos/Raft 等共识算法) | 中(异步复制 + 冲突检测) |
| 典型协议 | ZAB、Raft、Paxos | Gossip、Dynamo 风格 |
| 缓存场景适用性 | 金融交易、库存扣减 | 用户信息、商品详情、Feed 流 |
缓存一致性取舍:在缓存场景中,绝大多数业务选择最终一致性。因为缓存本身是性能加速层,如果为了强一致性引入分布式事务或共识协议,会大幅增加延迟,违背缓存设计的初衷。对于必须强一致的场景(如支付、库存),建议直接读数据库主库,或使用分布式锁+版本号机制。
最终一致性在缓存中的实现路径
- 异步 binlog 订阅:通过 Canal 监听 MySQL binlog,变更后异步删除/更新缓存。
- 消息队列通知:业务写 DB 后发送 MQ 消息,消费端更新缓存。
- 版本号/时间戳:缓存中存储数据版本号,读时对比版本,旧版本触发异步刷新。
- TTL 兜底:设置合理的过期时间,即使缓存不一致,也会在 TTL 后自动修复。
可观测性视角:在最终一致性方案中,不一致窗口的监控至关重要。通过 OpenTelemetry 追踪每次缓存更新操作的链路,记录“DB 更新时间”与“缓存更新时间”的差值,可以量化不一致窗口长度,并在窗口超过阈值时触发告警。这正是本课程后续章节的核心实践。