4. 缓存一致性问题:数据不一致的成因与典型场景

在分布式系统中,缓存与数据库之间的数据一致性是架构设计的核心挑战。当引入缓存层以提升读性能时,数据在缓存和数据库之间的同步延迟、并发冲突以及系统故障,都会导致两者数据产生偏差。本章将深入剖析缓存不一致的三大核心成因,并结合“读多写少”与“库存扣减”两个典型场景,为后续引入 TCC 模式奠定问题基础。

4.1 数据不一致的三大成因

缓存与数据库的数据不一致,本质上是由于两者在时间窗口内的状态分裂。根据 CAP 理论和实际工程经验,不一致的成因可归纳为以下三类:

成因 核心问题 典型表现 影响范围
并发写 多个写请求同时操作同一数据,导致缓存与数据库的更新顺序不一致 缓存中为旧值,数据库中为新值;或出现“写后写”覆盖丢失 高并发写入场景(如秒杀、点赞)
网络延迟 缓存与数据库之间的网络传输存在不可控的延迟,导致更新传播滞后 读请求命中缓存旧数据,数据库已更新但缓存未刷新 跨机房部署、异地多活架构
宕机/故障 缓存节点或数据库节点发生故障,导致更新操作中断或丢失 缓存失效后未回填,或数据库写入成功但缓存删除失败 任何依赖缓存加速的生产环境
关键认知: 缓存一致性问题的根源在于“缓存与数据库是两个独立的存储系统”,它们之间没有内置的分布式事务协议。任何单点写入、异步同步或最终一致性策略,都无法在理论上保证强一致性,只能通过补偿机制(如 TCC)将不一致窗口缩小到可接受范围。

4.2 并发写:更新顺序的竞态

并发写是缓存不一致最常见的成因。考虑一个典型的“先更新数据库,再删除缓存”策略:

// 线程A:更新数据库,然后删除缓存
UPDATE user SET balance = 100 WHERE id = 1;
DELETE cache:user:1;

// 线程B:同时读取并回填缓存
SELECT balance FROM user WHERE id = 1;  // 读到旧值 50
SET cache:user:1 = 50;                 // 回填旧值

当线程A删除缓存后,线程B恰好读取数据库(此时数据库已更新为100,但线程B可能因数据库主从延迟读到旧值50),并将旧值50回填到缓存。最终缓存中为50,数据库中为100,出现不一致。

工程启示: 并发写导致的不一致,通常需要引入“延迟双删”或“版本号校验”来缓解。但更根本的解法是让缓存更新操作具备“幂等性”和“顺序性”,这正是 TCC 模式中 Confirm/Cancel 阶段可以提供的保障。

4.3 网络延迟:时间窗口的放大

网络延迟将并发写的问题进一步放大。在分布式系统中,缓存与数据库之间的网络往返时间(RTT)通常在 0.1ms 到 10ms 之间,但在跨机房或公网环境下可能达到 50ms 以上。这个时间窗口内,任何读请求都可能命中过期的缓存数据。

网络延迟导致不一致的典型链路:

  1. 写请求 到达应用层,先更新数据库(耗时 2ms)。
  2. 网络传输 将“删除缓存”指令发送到缓存节点(耗时 5ms)。
  3. 在这 7ms 的窗口内,读请求 可能命中缓存中的旧数据。
  4. 如果缓存删除指令因网络抖动重试,窗口可能延长至 20ms 以上。
注意: 网络延迟不是随机事件,而是系统负载的函数。当缓存集群或数据库集群接近性能瓶颈时,网络队列堆积会导致延迟呈指数级增长,不一致窗口随之急剧扩大。这也是为什么高并发场景下,简单的“先删缓存再更新数据库”策略几乎必然导致数据不一致。

4.4 宕机:不可预测的状态分裂

宕机是缓存一致性中最难处理的问题,因为它会导致更新操作“半完成”。例如:

  • 数据库写入成功,缓存删除失败: 缓存中残留旧数据,后续读请求持续命中旧值,直到缓存过期。
  • 缓存删除成功,数据库写入失败: 缓存中数据被清空,但数据库未更新,后续读请求回填数据库旧值,导致缓存与数据库一致(均为旧值),但业务期望的新值丢失。
  • 节点宕机导致数据丢失: 缓存节点宕机后重启,所有数据丢失,大量读请求穿透到数据库,引发“缓存雪崩”。

宕机场景下的不一致,无法通过简单的重试或延迟策略解决,必须引入“补偿事务”或“最终一致性对账”机制。

4.5 典型场景一:读多写少

“读多写少”是缓存最经典的应用场景,例如用户信息、商品详情、配置数据等。这类场景下,不一致的主要风险来自:

  • 写操作频率低,但一旦发生,需要立即生效。 例如用户修改昵称后,期望所有页面立即显示新昵称。
  • 缓存过期时间较长(通常 5-30 分钟),导致不一致窗口被放大。
  • 读请求量巨大,缓存命中率要求极高,任何缓存失效都会导致数据库压力骤增。

在“读多写少”场景中,缓存不一致的典型表现是“写后读脏”:用户更新数据后,自己或他人读取到的仍是旧值。这种不一致虽然不会导致数据永久损坏,但会严重影响用户体验。

场景特征: 读 QPS 是写 QPS 的 100 倍以上;数据更新频率低(每小时几次到每天几次);对一致性要求为“最终一致性”,但期望延迟在秒级以内。

4.6 典型场景二:库存扣减

库存扣减是缓存一致性中最具挑战性的场景,因为它同时具备“高并发写”和“强一致性要求”两个特征:

维度 读多写少场景 库存扣减场景
写并发 低(每秒几十次) 极高(每秒数万次)
一致性要求 最终一致(秒级) 强一致(避免超卖)
数据敏感性 低(显示错误可容忍) 高(超卖导致资损)
缓存策略 缓存过期 + 惰性更新 缓存预扣 + 异步对账

库存扣减场景下,缓存不一致的直接后果是“超卖”:缓存中显示库存充足,但数据库中库存已不足,导致多个用户同时下单成功。传统的“先更新数据库,再删除缓存”策略在库存场景中几乎无效,因为:

  1. 高并发下,多个线程同时读取缓存中的库存值(如 10 件)。
  2. 每个线程都在本地扣减库存(如 10→9),然后尝试更新数据库。
  3. 数据库通过行锁或乐观锁保证只有一个线程成功,但其他线程的缓存扣减操作已经执行。
  4. 最终缓存中的库存值远低于数据库中的实际库存,导致“少卖”或“无法下单”。
核心矛盾: 库存扣减要求“缓存中的库存值”与“数据库中的库存值”在扣减瞬间保持一致,但缓存的高并发读取特性与数据库的事务隔离性天然冲突。这也是为什么库存场景必须引入“分布式事务”或“TCC 模式”来协调缓存与数据库的状态。

4.7 不一致成因与场景的关系矩阵

为了帮助理解不同成因在不同场景下的影响权重,下表总结了各成因在两类典型场景中的表现:

成因 读多写少场景 库存扣减场景
并发写 影响较小(写频率低) 核心风险(超卖的直接原因)
网络延迟 影响中等(窗口期用户感知旧数据) 影响极大(延迟导致缓存预扣与数据库扣减不同步)
宕机 影响较小(缓存重建成本低) 影响极大(宕机导致扣减记录丢失,对账困难)

4.8 不一致的演进路径:从最终一致到强一致

下图展示了缓存不一致问题从产生到演进的完整路径,以及不同阶段对应的解决策略:

缓存不一致演进路径与解决策略 并发写 网络延迟 宕机/故障 缓存与数据库值分裂 读请求命中旧数据 更新操作半完成 用户体验下降 数据不一致 资损/超卖 延迟双删 / 版本号 异步对账 / 补偿 TCC 分布式事务

4.9 小结:不一致问题的本质

缓存一致性问题的本质,是“缓存层”与“数据库层”在时间维度上的状态分裂。无论是并发写、网络延迟还是宕机,最终都指向同一个核心矛盾:缓存的高性能读取特性与数据库的事务强一致性特性无法天然兼容。