4. 缓存一致性问题的由来:读写并发导致的数据不一致、缓存与数据库状态不同步

在引入缓存层后,系统从“单一数据库”变成了“缓存 + 数据库”的双存储架构。由于缓存和数据库是两个独立的存储系统,且操作并非原子性,当并发读写发生时,很容易出现数据不一致的问题。本章将深入剖析这一问题的根源。

4.1 核心矛盾:双存储与原子性缺失

缓存一致性问题本质上源于以下两个事实:

  • 双存储状态:同一份数据在缓存(Redis)和数据库(MySQL)中各有一份副本。
  • 非原子操作:更新缓存和更新数据库是两个独立的步骤,无法在一个数据库事务中完成。

当多个线程或进程同时对同一份数据进行读写操作时,操作顺序的交叉会导致缓存与数据库中的值产生永久性差异。

核心结论: 只要缓存与数据库是两套独立的存储系统,且更新操作无法原子化,就必然存在一致性问题。我们的目标是找到一种策略,将不一致的窗口缩小到可接受的范围,甚至完全消除。

4.2 典型场景:读写并发冲突

最常见的冲突场景是“读线程”与“写线程”同时操作同一条数据。下面通过一个具体的时序图来展示问题是如何发生的。

读写并发导致缓存与数据库不一致 应用线程A (写) 应用线程B (读) Redis / MySQL 1. 更新DB DB: v=2 2. 读缓存 缓存: v=1 (旧) 3. 返回旧值 4. 删除缓存 缓存: 已删除 5. 重建缓存 缓存: v=1 (旧) 结果:DB中v=2,缓存中v=1,不一致!

上图展示了一个经典的读写并发问题:

  1. 写线程先更新了数据库(v=1 → v=2)。
  2. 此时读线程读取缓存,发现缓存中仍是旧值(v=1)。
  3. 读线程将旧值(v=1)返回给上层应用。
  4. 写线程随后删除了缓存。
  5. 读线程在缓存删除后,将刚才读到的旧值(v=1)重新写入缓存。

最终,数据库中是 v=2,缓存中却是 v=1,数据永久不一致。

4.3 缓存与数据库状态不同步的三种形态

根据操作顺序和失败情况的不同,不一致问题可以归纳为以下三种形态:

形态 描述 典型场景
缓存脏数据 缓存中存有旧值,数据库已更新为新值 先更新数据库,后删除缓存,但删除失败
缓存缺失 缓存被清空,但数据库有最新数据 正常删除缓存后,下次读取会回填最新值,通常无害
缓存与数据库永久不一致 缓存和数据库的值不同,且无法通过后续操作自动修复 读写并发导致旧值回填(如上图所示)
注意: 第三种形态“永久不一致”是最危险的。它不会因为后续的读取操作而自动修复,因为缓存中的旧值会持续被读取,直到下一次写操作触发缓存更新。这种问题在业务上可能导致用户看到过时信息、重复扣款、库存超卖等严重故障。

4.4 不一致问题的根本原因分析

从架构层面看,缓存一致性问题可以拆解为以下三个根本原因:

  • 操作顺序不确定:先更新数据库还是先更新缓存?不同的顺序会导致不同的并发风险。
  • 缺乏互斥锁:读操作和写操作之间没有锁机制,导致读操作可能在写操作的“中间状态”读取数据。
  • 失败处理缺失:当缓存操作(如删除、更新)失败时,没有补偿机制,导致数据永久不一致。

下面的表格对比了两种基本操作顺序的优缺点:

策略 操作顺序 并发风险 典型应用
先更新DB,后删除缓存 1. 更新DB → 2. 删除缓存 删除缓存失败会导致脏数据 Cache-Aside 模式
先删除缓存,后更新DB 1. 删除缓存 → 2. 更新DB 并发读可能回填旧值 延迟双删策略

4.5 问题复现的典型条件

在实际生产环境中,以下条件会显著增加缓存不一致问题的发生概率:

  • 高并发读写:同一热点数据被大量线程同时读写。
  • 长事务写操作:写线程更新数据库耗时较长,增加了并发窗口。
  • 缓存操作失败:网络抖动、Redis 超时导致缓存删除或更新失败。
  • 缓存重建耗时:从数据库读取数据并写入缓存的过程耗时较长,增加了被其他线程干扰的概率。
实践建议: 在排查缓存不一致问题时,可以优先检查上述四个条件。通常,降低并发窗口(如使用分布式锁)或保证缓存操作的可靠性(如重试机制)是解决问题的关键方向。

4.6 小结