场景:为什么会出现不一致?


前置知识: 无 难度: ⭐⭐ 预计时间: 30分钟 关键词: 缓存不一致, 并发, 竞态条件, 数据库, Redis

引入场景

你负责一个用户服务。数据存在MySQL中,用Redis做缓存加速读取。某天你收到反馈:“用户改了头像,但页面上看到的还是旧的。“你查了MySQL——数据已更新。查了Redis——缓存还是旧值。

为什么数据库和缓存会不一致?

这不是Bug,而是分布式系统的根本挑战:任何时候你把数据存了两份,它们就可能不一致。

对话探索

问题1:最简单的方案——更新数据库后立即更新缓存。这有什么问题?

💭 思考方向:

  • 如果两个请求同时更新同一条数据?
  • 更新数据库成功但更新缓存失败呢?
  • 多个服务器时,谁先谁后?

📖 参考答案:

并发更新导致数据错乱:

时间线:
  请求A(将价格改为100)          请求B(将价格改为200)
  ──────────────────          ──────────────────
  1. 更新DB: price=100
                               2. 更新DB: price=200
                               3. 更新缓存: price=200
  4. 更新缓存: price=100       
  
  最终结果:DB=200, 缓存=100  ← 不一致!

请求A的缓存更新晚于请求B(虽然A先更新了数据库),导致缓存存了过时的值。

部分失败更糟糕:

更新DB成功 → 更新缓存失败(网络抖动)
结果:DB是新值,缓存是旧值
而且你可能根本不知道缓存更新失败了

问题2:那改成”更新数据库后删除缓存”呢?下次读取时重新从DB加载。

💭 思考方向:

  • 删除缓存比更新缓存好在哪里?
  • 还有并发问题吗?
  • 先删缓存还是先更新DB?

📖 参考答案:

Cache-Aside模式(最常用):

写操作:更新DB → 删除缓存
读操作:查缓存 → 缓存没有 → 查DB → 写入缓存

这比”更新缓存”好,因为删除是幂等的(删多次也没关系),而更新不是。

但仍有并发问题:

请求A(读)                      请求B(写)
─────────────                   ──────────────
1. 查缓存 → 缓存未命中
2. 查DB → 得到旧值
                                3. 更新DB为新值
                                4. 删除缓存
5. 将旧值写入缓存               
                                
结果:DB=新值, 缓存=旧值 ← 不一致!

请求A在步骤2查到旧值后,被请求B抢先更新了DB并删了缓存,然后A才把旧值写入缓存——产生了过时数据。

不过这个窗口期很短(需要”读DB”恰好在”写DB+删缓存”之间),实践中出现的概率较低。


问题3:那”先删缓存,再更新数据库”呢?会更好吗?

💭 思考方向:

  • 删完缓存后、更新DB前,如果有读请求进来?
  • 读请求会从DB读到旧值并写入缓存
  • 这个时间窗口有多大?

📖 参考答案:

请求A(写)                      请求B(读)
──────────────                  ─────────────
1. 删除缓存                      
                                2. 查缓存 → 未命中
                                3. 查DB → 旧值(DB还没更新!)
                                4. 将旧值写入缓存
5. 更新DB为新值                  
                                
结果:DB=新值, 缓存=旧值 ← 不一致!而且持续到缓存过期

这比”先更新DB再删缓存”更糟——不一致的时间窗口更大(整个DB写操作期间),且这种序列更容易出现。

小结:

方案出现不一致的概率不一致持续时间
先更新缓存高(并发乱序)直到下次更新
先更新DB,再删缓存低(窗口极短)直到缓存过期
先删缓存,再更新DB中(窗口较大)直到缓存过期

问题4:有没有办法完全消除不一致?

💭 思考方向:

  • 分布式事务?
  • 性能代价?
  • “完全一致”在分布式系统中可能吗?

📖 参考答案:

理论上:可以用分布式事务(2PC)保证DB和缓存强一致。 实际上:几乎没人这么做。原因:

  1. 性能代价太高:每次写操作都需要分布式锁/2PC → 延迟增加数倍
  2. 可用性降低:Redis或DB任一不可用,写操作就阻塞
  3. 本末倒置:引入缓存就是为了提升性能,如果为了一致性牺牲性能,不如直接查DB

现实做法:接受最终一致性,将不一致窗口缩到业务可接受的范围。

实际策略组合:
1. Cache-Aside + TTL(缓存过期时间)
   → 即使不一致,最多持续TTL时间
   
2. 延迟双删
   → 进一步缩小不一致窗口
   
3. 订阅数据库变更(binlog)
   → 数据库变了就通知清缓存

问题5:除了DB-缓存,还有哪些场景会出现不一致?

💭 思考方向:

  • 微服务间的数据同步
  • 搜索引擎索引和数据库
  • CDN缓存和源站

📖 参考答案:

万物皆可不一致——只要数据存了两份以上:

场景数据源A数据源B一致性手段
DB+缓存MySQLRedisCache-Aside + TTL
DB+搜索MySQLElasticsearchBinlog订阅 / 定时同步
DB+CDN源站CDN节点主动清除 / TTL
主从复制主库从库复制滞后 → 读主库
微服务订单服务库存服务事件驱动 / Saga

核心认识:不一致不是Bug,而是分布式系统的固有属性。工程师的工作是选择合适的策略,将不一致控制在业务可接受的范围内。

这就引出了理论层的深入讨论——一致性模型。

小结

⭐ 核心要点:

  1. 数据一致性问题的根源:多份数据 + 并发修改 + 网络不可靠
  2. 没有完美方案:每种缓存策略都有不一致窗口,区别只是大小
  3. 工程思维:接受最终一致性,用多种手段(TTL、双删、binlog)将窗口缩到可接受范围

关联阅读