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. 网络不确定性:请求可能丢失、重试、乱序到达。缓存与数据库之间的写操作无法保证原子性。
  2. 并发写入冲突:多个服务实例同时更新同一份数据,缓存与数据库的更新顺序无法全局排序。
  3. 缓存与数据库独立生命周期:缓存过期、淘汰、重建等操作与数据库事务不在同一个事务上下文中。
分布式缓存不一致三大根源 根源一:网络不确定性 请求丢失 / 重试 / 乱序 根源二:并发写入冲突 多实例同时更新同一数据 根源三:独立生命周期 缓存过期 / 淘汰 / 重建 缓存与数据库写操作无法保证原子性 & 全局排序 最终导致:读请求可能拿到旧数据(Stale Read) 后果一:脏读(Dirty Read) 读到未提交或已回滚的中间数据 后果二:不可重复读(Non-Repeatable Read) 同一事务内两次读取结果不一致 后果三:缓存穿透 / 击穿 / 雪崩 缓存层失效导致数据库压力骤增

1.4 典型不一致场景:写后读失效

这是生产环境中最常见的一类问题。用户写入数据后立即读取,却拿到了旧值。其发生路径如下:

  1. 服务 A 更新数据库中的用户余额(写 DB)。
  2. 服务 A 删除或更新缓存中的对应 Key。
  3. 在缓存更新完成前,服务 B 读取该 Key,命中旧缓存。
  4. 服务 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 缓存到分布式缓存都存在此问题。
  • 分布式环境下三大根源:网络不确定性、并发写入冲突、独立生命周期。
  • 典型场景包括:写后读失效、双写冲突、主从延迟 + 缓存重建。
  • 不同一致性模型对应不同的性能与一致性权衡,架构师需要根据业务场景做出合理选择。