1. 缓存一致性问题全景:从单机到分布式,缓存不一致的根源与典型场景
1.1 缓存一致性的本质:数据多副本下的时序冲突
缓存一致性问题,本质上是同一份数据在多个存储副本(缓存层与持久层)之间,因更新时序不同步而产生的数据偏差。在单机环境下,由于内存与磁盘的访问速度差异,缓存一致性问题相对可控;但在分布式系统中,网络延迟、节点故障、并发写入等因素使得一致性问题变得极为复杂。
核心矛盾:缓存追求高性能(读快、写快),数据库追求持久性与事务性(ACID)。两者之间的写后读一致性(Read-Your-Writes Consistency)是最常被打破的约束。
1.2 单机缓存不一致:从 CPU 到应用层
即使在单机范围内,缓存不一致也普遍存在。下表梳理了从硬件到软件各层级的典型不一致场景:
| 层级 | 缓存载体 | 不一致根源 | 典型场景 |
|---|---|---|---|
| CPU 缓存 | L1/L2/L3 Cache | 多核缓存一致性协议(MESI)失效或 Store Buffer 延迟 | 多线程共享变量未加 volatile 导致可见性问题 |
| 操作系统 | Page Cache / Buffer Cache | write-back 策略下脏页未刷盘 | 系统崩溃导致文件写入丢失 |
| 应用进程 | 本地堆内缓存(如 HashMap、Guava Cache) | 多线程并发读写无锁保护 | Web 应用本地缓存未同步导致数据过期 |
| 数据库 | InnoDB Buffer Pool | Redo Log 异步刷盘 + MVCC 快照读 | 主从延迟下的读写分离脏读 |
工程启示:单机缓存不一致的解决思路通常是内存屏障(Memory Barrier)或锁。但在分布式场景下,这些手段成本过高,需要引入更轻量的最终一致性策略。
1.3 分布式缓存不一致:三大根源
分布式环境下,缓存不一致的根源可以归纳为以下三类:
- 网络不确定性:请求可能丢失、重试、乱序到达。缓存与数据库之间的写操作无法保证原子性。
- 并发写入冲突:多个服务实例同时更新同一份数据,缓存与数据库的更新顺序无法全局排序。
- 缓存与数据库独立生命周期:缓存过期、淘汰、重建等操作与数据库事务不在同一个事务上下文中。
1.4 典型不一致场景:写后读失效
这是生产环境中最常见的一类问题。用户写入数据后立即读取,却拿到了旧值。其发生路径如下:
- 服务 A 更新数据库中的用户余额(写 DB)。
- 服务 A 删除或更新缓存中的对应 Key。
- 在缓存更新完成前,服务 B 读取该 Key,命中旧缓存。
- 服务 B 返回旧余额给用户,造成数据不一致。
避坑提示:很多团队采用“先更新数据库,再删除缓存”的策略(Cache-Aside 模式)。但若步骤 2 失败(如网络超时),缓存中仍为旧数据。此时需要引入重试机制或延迟双删来兜底。
1.5 典型不一致场景:缓存与数据库双写冲突
当多个服务实例并发执行“写 DB + 写缓存”操作时,时序交错会导致数据永久不一致:
| 时间线 | 线程 A(写操作) | 线程 B(写操作) | 缓存最终值 | 数据库最终值 |
|---|---|---|---|---|
| T1 | 写 DB:余额 = 100 | — | — | 100 |
| T2 | — | 写 DB:余额 = 200 | — | 200 |
| T3 | 写缓存:余额 = 100 | — | 100 | 200 |
| T4 | — | 写缓存:余额 = 200 | 200 | 200 |
上例中,线程 A 在 T3 时刻将旧值 100 写入缓存,覆盖了线程 B 在 T4 时刻写入的正确值 200。最终缓存与数据库一致(均为 200),但中间状态出现了短暂的不一致窗口。若在 T3 与 T4 之间有读请求,就会读到 100 这个过期值。
1.6 典型不一致场景:主从延迟 + 缓存重建
在读写分离架构中,主库写入后,从库存在复制延迟。若缓存重建时读取从库,可能拿到未同步的旧数据:
// 伪代码:缓存重建时可能读到从库旧数据
public User getUser(String userId) {
User user = cache.get(userId);
if (user == null) {
// 从从库读取,可能因主从延迟拿到旧数据
user = slaveDb.query("SELECT * FROM user WHERE id = ?", userId);
cache.set(userId, user, 300); // 缓存中写入旧数据
}
return user;
}
解决方案方向:缓存重建时强制读主库,或使用版本号/时间戳校验数据新鲜度,拒绝写入过期数据。
1.7 缓存一致性分类与权衡
不同业务场景对一致性的容忍度不同。下表梳理了常见的缓存一致性模型及其适用场景:
| 一致性模型 | 定义 | 典型实现 | 适用场景 |
|---|---|---|---|
| 强一致性 | 写入后立即对所有读可见 | 分布式锁 + 同步双写 | 金融交易、库存扣减 |
| 最终一致性 | 写入后经过一段窗口最终一致 | Cache-Aside + 异步补偿 | 社交动态、内容推荐 |
| 读写一致性 | 写入者自己立即读到最新值 | Write-Through + Session Stick | 用户个人设置、购物车 |
| 单调读一致性 | 一旦读到某个值,后续不会读到更旧的值 | 时间戳 + 版本号校验 | 消息推送、通知列表 |
架构决策原则:不要追求“绝对一致”,而是根据业务对一致性等级的要求,选择最经济的实现方案。90% 的场景下,最终一致性配合合理的补偿机制即可满足需求。
1.8 本章小结
- 缓存一致性的本质是多副本时序冲突,从单机 CPU 缓存到分布式缓存都存在此问题。
- 分布式环境下三大根源:网络不确定性、并发写入冲突、独立生命周期。
- 典型场景包括:写后读失效、双写冲突、主从延迟 + 缓存重建。
- 不同一致性模型对应不同的性能与一致性权衡,架构师需要根据业务场景做出合理选择。