2. 缓存一致性问题的根源:数据竞争、并发写入、过期策略导致的脏读与幻读
缓存一致性问题的本质,是数据在多个存储层级(L1 Cache、L2 Cache、DB)之间产生了不可调和的差异。这种差异并非偶然,而是由三个核心因素共同作用的结果:数据竞争、并发写入与过期策略。理解这些根源,是设计一致性方案的前提。
2.1 数据竞争:无锁环境下的状态撕裂
当多个线程或进程同时访问同一份缓存数据,且至少有一个是写操作时,就产生了数据竞争。在无锁或弱锁环境下,这种竞争会导致缓存中的中间状态被其他线程读取,形成脏读。
关键指标: 数据竞争发生的概率与并发线程数、缓存行大小、写操作耗时成正比。在微服务架构中,跨进程的缓存竞争比单进程内更难以控制。
2.2 并发写入:写写冲突与更新丢失
当多个写入者同时修改同一份缓存数据时,会发生写写冲突。常见的后果包括:
- 更新丢失:后写入的数据覆盖了先写入的数据,但先写入的数据才是正确的。
- 写入顺序错乱:缓存与数据库的写入顺序不一致,导致缓存与DB永久不一致。
- 缓存击穿:大量并发写入导致缓存失效,瞬间流量全部打到数据库。
| 并发写入场景 | 典型后果 | 影响级别 |
|---|---|---|
| 先删缓存,再写DB | 其他线程在删缓存后、写DB前读DB旧值并回填缓存 | 高 |
| 先写DB,再删缓存 | 写DB成功但删缓存失败,缓存保留旧值 | 中 |
| 双写(同时写缓存和DB) | 无事务保护时,缓存与DB写入顺序不一致 | 高 |
| 批量写入 + 部分失败 | 缓存部分更新,DB全部更新,产生数据碎片 | 极高 |
避坑提示: 很多团队在初期采用“先删缓存,再写DB”的策略,认为这样简单。但在高并发下,这恰恰是最容易产生脏读的模式。推荐使用“延迟双删”或“先写DB,再删缓存”配合消息队列补偿。
2.3 过期策略:TTL 与惰性删除的陷阱
缓存过期策略本身是为了保证数据最终一致性,但不当的配置会放大一致性问题:
- TTL 过长:缓存长期持有旧数据,DB已更新但缓存未感知,导致幻读(读到已不存在的记录)或不可重复读。
- TTL 过短:缓存频繁失效,导致缓存命中率下降,DB压力增大,间接增加数据竞争窗口。
- 惰性删除:只在读取时检查过期,过期数据可能长时间驻留内存,被后续请求读到。
- 主动删除 + 并发:缓存主动淘汰时,恰好有写入请求,可能导致淘汰了最新数据,保留了旧数据。
2.4 三种根源的对比与关联
| 根源 | 触发条件 | 主要后果 | 典型场景 |
|---|---|---|---|
| 数据竞争 | 无锁并发读写同一缓存行 | 脏读、中间状态泄露 | 多线程共享缓存、无原子操作 |
| 并发写入 | 多写入者同时修改同一缓存 | 更新丢失、写入顺序错乱 | 高并发写热点、分布式锁缺失 |
| 过期策略 | TTL配置不当、惰性删除 | 幻读、不可重复读 | 长TTL缓存、DB已更新但缓存未失效 |
实践建议: 在诊断缓存一致性问题时,先判断是“数据竞争”还是“过期策略”导致的。数据竞争通常表现为偶发、高频的脏读;过期策略导致的幻读通常表现为持续、可预测的旧数据返回。区分这两者,能快速定位解决方案方向。
2.5 根源的叠加效应
在实际生产环境中,这三种根源往往不是独立存在的,而是叠加放大:
- 数据竞争 + 过期策略:竞争导致缓存写入延迟,过期策略又无法及时清理旧数据,脏读窗口被拉长。
- 并发写入 + 数据竞争:多个写入者竞争同一缓存行,同时读线程也在竞争,形成“读写锁”级别的混乱。
- 三者叠加:高并发写入热点数据,TTL设置过长,且没有原子操作保护——这是缓存一致性灾难的典型配方。