4. 缓存与数据库双写一致性问题

在读写缓存 + 数据库的架构中,双写一致性 是指对同一份数据的数据库写入操作与缓存更新操作之间,最终或强一致地反映相同的数据状态。由于缓存和数据库是两个独立的存储系统,无法通过单机事务保证原子性,因此必须通过特定的更新顺序和补偿策略来降低不一致窗口。

4.1 核心矛盾:更新顺序的选择

当业务执行写操作时,通常涉及两个步骤:修改数据库(DB)和操作缓存(Cache)。根据操作顺序的不同,存在两种经典方案,各有优劣。

方案 执行顺序 核心风险 不一致窗口
先写 DB 后删缓存 ① 更新 DB → ② 删除 Cache 删除缓存失败导致数据脏读 极小(删除前瞬间)
先删缓存后写 DB ① 删除 Cache → ② 更新 DB 并发读请求回填旧数据 较大(写 DB 完成前)

关键结论: 先写 DB 后删缓存(Cache-Aside 模式的标准做法)在绝大多数场景下优于先删缓存后写 DB,因为前者仅在删除缓存失败时产生不一致,而后者在并发读请求下极易回填旧数据,导致长时间脏读。

4.2 先写 DB 后删缓存(推荐方案)

这是业界最广泛采用的缓存更新策略,也是 Cache-Aside Pattern 的标准写路径。其流程如下:

  1. 应用程序直接更新数据库中的记录。
  2. 更新成功后,立即删除缓存中对应的 key。
  3. 后续读请求发现缓存缺失,从数据库加载最新数据并回填缓存。

为什么是删除而不是更新? 删除操作是幂等的,且避免了一次无意义的缓存写入。如果更新缓存,可能因并发写顺序导致缓存与数据库不一致。删除缓存后,下次读请求自然会拉取最新数据。

唯一风险: 步骤 ② 删除缓存可能失败(如网络抖动、Redis 超时)。此时缓存中残留旧数据,后续读请求会命中脏数据。解决方案见下文 延迟双删策略。

4.3 先删缓存后写 DB(不推荐但需理解)

部分早期设计采用此顺序,意图是让写操作立即失效缓存,避免后续读请求读到旧数据。然而,该方案存在严重的并发一致性问题:

  1. 线程 A 删除缓存 key。
  2. 线程 B(读请求)发现缓存缺失,从 DB 读取旧数据并回填缓存。
  3. 线程 A 完成 DB 更新,但缓存中已被线程 B 写入了旧数据。

结果:缓存中长期存储旧数据,直到下一次写操作或缓存过期才能修复。这种不一致窗口远大于先写 DB 后删缓存方案。

避坑提醒: 除非你能保证在删除缓存到更新 DB 的极短窗口内没有任何读请求(例如通过分布式锁或队列串行化),否则不要使用先删缓存后写 DB。在大多数高并发业务中,该方案会导致频繁的缓存与数据库不一致。

4.4 延迟双删策略(Delayed Double Delete)

延迟双删是针对 先写 DB 后删缓存 方案中删除失败风险的补偿机制。其核心思想是:在第一次删除缓存后,等待一段时间再次删除,确保即使第一次删除失败,也能通过第二次删除修复。

标准执行流程:

  1. 更新数据库。
  2. 第一次删除缓存 key。
  3. 休眠一段时间(通常为 500ms ~ 2s,取决于业务读延迟)。
  4. 第二次删除同一个缓存 key。

延迟时间如何设定? 延迟时间应大于 从数据库读取数据 + 回填缓存 的最大耗时。通常建议设置为 1 秒左右。如果业务对一致性要求极高,可适当延长至 2~3 秒,但需权衡写入延迟。

延迟双删并不能保证 100% 强一致,但它将不一致窗口从 永久(删除失败) 缩小到 延迟时间窗口内。配合缓存过期时间,最终一致性可以得到保障。

延迟双删策略执行流程 应用服务 数据库 Redis 缓存 ① 更新 DB ② 第一次删除 ③ 休眠延迟(如 1s) Redis 缓存 ④ 第二次删除 补偿效果 即使第一次删除失败,第二次删除也能在延迟后清除可能被并发读回填的旧数据

4.5 最终一致性原理

在缓存与数据库架构中,强一致性 通常难以实现(除非引入分布式事务或 2PC,但会严重牺牲性能)。实际生产环境普遍接受 最终一致性,即允许在短时间内存在不一致,但保证经过一定时间后数据最终一致。

最终一致性的实现依赖以下机制:

机制 作用 典型配置
缓存过期时间(TTL) 强制缓存失效,触发重新加载 业务可接受的最大不一致时间,如 5~30 分钟
延迟双删 补偿删除失败或并发回填旧数据 延迟 1~3 秒,异步执行
异步消息补偿 通过 MQ 消息确保删除操作最终被执行 删除失败后发送重试消息
Binlog 监听(Canal) 从数据库变更日志捕获变更,异步刷新缓存 监听 MySQL binlog,推送到 Redis

最终一致性的时间窗口: 在延迟双删 + TTL 的配合下,不一致窗口通常被控制在 1~3 秒 内。对于绝大多数互联网业务(如商品详情、用户信息),这个窗口是可接受的。对于金融、库存等对一致性要求极高的场景,应使用分布式锁或数据库强一致方案。

4.6 方案对比总结

方案 一致性保证 并发性能 实现复杂度 适用场景
先写 DB 后删缓存 最终一致(删除失败时需补偿) 高 低 通用场景,推荐默认使用
先删缓存后写 DB 弱一致(并发下易脏读) 中 低 不推荐,除非写串行化
延迟双删 最终一致(窗口可控) 中(增加一次删除延迟) 中 对一致性要求较高的业务
Binlog 监听异步刷新 最终一致(秒级延迟) 高(异步解耦) 高 大型系统,需要解耦写路径

重要提醒: 没有任何一种缓存更新策略能保证 100% 强一致且高性能。架构师需要根据业务对一致性的容忍度、并发量、运维成本来选择合适的方案。对于绝大多数场景,先写 DB 后删缓存 + 延迟双删 + 合理 TTL 是性价比最高的组合。