2. 缓存一致性问题的根源:数据竞争、并发写入、过期策略导致的脏读与幻读

缓存一致性问题的本质,是数据在多个存储层级(L1 Cache、L2 Cache、DB)之间产生了不可调和的差异。这种差异并非偶然,而是由三个核心因素共同作用的结果:数据竞争、并发写入与过期策略。理解这些根源,是设计一致性方案的前提。

2.1 数据竞争:无锁环境下的状态撕裂

当多个线程或进程同时访问同一份缓存数据,且至少有一个是写操作时,就产生了数据竞争。在无锁或弱锁环境下,这种竞争会导致缓存中的中间状态被其他线程读取,形成脏读。

关键指标: 数据竞争发生的概率与并发线程数、缓存行大小、写操作耗时成正比。在微服务架构中,跨进程的缓存竞争比单进程内更难以控制。
数据竞争导致脏读的时序图 数据竞争导致脏读的典型时序 线程A (写) 线程B (读) 缓存状态 写开始 读请求 写完成 旧值 脏值 新值 线程B在“写开始”与“写完成”之间读取到缓存中的“脏值”,导致脏读。 即使写操作最终成功,读操作已经看到了不一致的状态。

2.2 并发写入:写写冲突与更新丢失

当多个写入者同时修改同一份缓存数据时,会发生写写冲突。常见的后果包括:

  • 更新丢失:后写入的数据覆盖了先写入的数据,但先写入的数据才是正确的。
  • 写入顺序错乱:缓存与数据库的写入顺序不一致,导致缓存与DB永久不一致。
  • 缓存击穿:大量并发写入导致缓存失效,瞬间流量全部打到数据库。
并发写入场景 典型后果 影响级别
先删缓存,再写DB 其他线程在删缓存后、写DB前读DB旧值并回填缓存 高
先写DB,再删缓存 写DB成功但删缓存失败,缓存保留旧值 中
双写(同时写缓存和DB) 无事务保护时,缓存与DB写入顺序不一致 高
批量写入 + 部分失败 缓存部分更新,DB全部更新,产生数据碎片 极高
避坑提示: 很多团队在初期采用“先删缓存,再写DB”的策略,认为这样简单。但在高并发下,这恰恰是最容易产生脏读的模式。推荐使用“延迟双删”或“先写DB,再删缓存”配合消息队列补偿。

2.3 过期策略:TTL 与惰性删除的陷阱

缓存过期策略本身是为了保证数据最终一致性,但不当的配置会放大一致性问题:

  • TTL 过长:缓存长期持有旧数据,DB已更新但缓存未感知,导致幻读(读到已不存在的记录)或不可重复读。
  • TTL 过短:缓存频繁失效,导致缓存命中率下降,DB压力增大,间接增加数据竞争窗口。
  • 惰性删除:只在读取时检查过期,过期数据可能长时间驻留内存,被后续请求读到。
  • 主动删除 + 并发:缓存主动淘汰时,恰好有写入请求,可能导致淘汰了最新数据,保留了旧数据。
过期策略导致幻读的流程 过期策略导致幻读的典型流程 DB更新记录R (状态: 已删除) 缓存未过期 (TTL=300s, 剩余200s) 读请求到达 (读到缓存中的旧记录R) 幻读产生条件 1. DB中记录R已被删除或更新,但缓存中的旧记录R尚未过期。 2. 读请求命中缓存,返回了“已不存在”的数据,产生幻读。 3. 直到缓存过期或主动淘汰,幻读现象才会消失。 解决方案:缩短TTL + 主动失效 + 版本号校验。

2.4 三种根源的对比与关联

根源 触发条件 主要后果 典型场景
数据竞争 无锁并发读写同一缓存行 脏读、中间状态泄露 多线程共享缓存、无原子操作
并发写入 多写入者同时修改同一缓存 更新丢失、写入顺序错乱 高并发写热点、分布式锁缺失
过期策略 TTL配置不当、惰性删除 幻读、不可重复读 长TTL缓存、DB已更新但缓存未失效
实践建议: 在诊断缓存一致性问题时,先判断是“数据竞争”还是“过期策略”导致的。数据竞争通常表现为偶发、高频的脏读;过期策略导致的幻读通常表现为持续、可预测的旧数据返回。区分这两者,能快速定位解决方案方向。

2.5 根源的叠加效应

在实际生产环境中,这三种根源往往不是独立存在的,而是叠加放大:

  • 数据竞争 + 过期策略:竞争导致缓存写入延迟,过期策略又无法及时清理旧数据,脏读窗口被拉长。
  • 并发写入 + 数据竞争:多个写入者竞争同一缓存行,同时读线程也在竞争,形成“读写锁”级别的混乱。
  • 三者叠加:高并发写入热点数据,TTL设置过长,且没有原子操作保护——这是缓存一致性灾难的典型配方。