4. 缓存一致性问题的由来:读写并发导致的数据不一致、缓存与数据库状态不同步
在引入缓存层后,系统从“单一数据库”变成了“缓存 + 数据库”的双存储架构。由于缓存和数据库是两个独立的存储系统,且操作并非原子性,当并发读写发生时,很容易出现数据不一致的问题。本章将深入剖析这一问题的根源。
4.1 核心矛盾:双存储与原子性缺失
缓存一致性问题本质上源于以下两个事实:
- 双存储状态:同一份数据在缓存(Redis)和数据库(MySQL)中各有一份副本。
- 非原子操作:更新缓存和更新数据库是两个独立的步骤,无法在一个数据库事务中完成。
当多个线程或进程同时对同一份数据进行读写操作时,操作顺序的交叉会导致缓存与数据库中的值产生永久性差异。
核心结论: 只要缓存与数据库是两套独立的存储系统,且更新操作无法原子化,就必然存在一致性问题。我们的目标是找到一种策略,将不一致的窗口缩小到可接受的范围,甚至完全消除。
4.2 典型场景:读写并发冲突
最常见的冲突场景是“读线程”与“写线程”同时操作同一条数据。下面通过一个具体的时序图来展示问题是如何发生的。
上图展示了一个经典的读写并发问题:
- 写线程先更新了数据库(v=1 → v=2)。
- 此时读线程读取缓存,发现缓存中仍是旧值(v=1)。
- 读线程将旧值(v=1)返回给上层应用。
- 写线程随后删除了缓存。
- 读线程在缓存删除后,将刚才读到的旧值(v=1)重新写入缓存。
最终,数据库中是 v=2,缓存中却是 v=1,数据永久不一致。
4.3 缓存与数据库状态不同步的三种形态
根据操作顺序和失败情况的不同,不一致问题可以归纳为以下三种形态:
| 形态 | 描述 | 典型场景 |
|---|---|---|
| 缓存脏数据 | 缓存中存有旧值,数据库已更新为新值 | 先更新数据库,后删除缓存,但删除失败 |
| 缓存缺失 | 缓存被清空,但数据库有最新数据 | 正常删除缓存后,下次读取会回填最新值,通常无害 |
| 缓存与数据库永久不一致 | 缓存和数据库的值不同,且无法通过后续操作自动修复 | 读写并发导致旧值回填(如上图所示) |
注意: 第三种形态“永久不一致”是最危险的。它不会因为后续的读取操作而自动修复,因为缓存中的旧值会持续被读取,直到下一次写操作触发缓存更新。这种问题在业务上可能导致用户看到过时信息、重复扣款、库存超卖等严重故障。
4.4 不一致问题的根本原因分析
从架构层面看,缓存一致性问题可以拆解为以下三个根本原因:
- 操作顺序不确定:先更新数据库还是先更新缓存?不同的顺序会导致不同的并发风险。
- 缺乏互斥锁:读操作和写操作之间没有锁机制,导致读操作可能在写操作的“中间状态”读取数据。
- 失败处理缺失:当缓存操作(如删除、更新)失败时,没有补偿机制,导致数据永久不一致。
下面的表格对比了两种基本操作顺序的优缺点:
| 策略 | 操作顺序 | 并发风险 | 典型应用 |
|---|---|---|---|
| 先更新DB,后删除缓存 | 1. 更新DB → 2. 删除缓存 | 删除缓存失败会导致脏数据 | Cache-Aside 模式 |
| 先删除缓存,后更新DB | 1. 删除缓存 → 2. 更新DB | 并发读可能回填旧值 | 延迟双删策略 |
4.5 问题复现的典型条件
在实际生产环境中,以下条件会显著增加缓存不一致问题的发生概率:
- 高并发读写:同一热点数据被大量线程同时读写。
- 长事务写操作:写线程更新数据库耗时较长,增加了并发窗口。
- 缓存操作失败:网络抖动、Redis 超时导致缓存删除或更新失败。
- 缓存重建耗时:从数据库读取数据并写入缓存的过程耗时较长,增加了被其他线程干扰的概率。
实践建议: 在排查缓存不一致问题时,可以优先检查上述四个条件。通常,降低并发窗口(如使用分布式锁)或保证缓存操作的可靠性(如重试机制)是解决问题的关键方向。