缓存双写策略(Cache Dual Write Strategy)是解决 缓存(如 Redis)与数据库(如 MySQL)数据一致性 的核心方案之一。它指的是 在数据更新时,同时(或按特定顺序)更新缓存和数据库 的操作策略。
📌 为什么需要双写?核心矛盾
-
性能需求: 读操作远多于写操作,缓存提供高性能读取。
-
数据持久性需求: 数据库保证数据的持久化和强一致性。
-
一致性需求: 用户期望读取缓存和数据库的结果是一致的(尤其是在更新后)。
🔧 常见的缓存双写策略及其特点
主要分为两大类:同步双写 和 异步双写(通过消息队列),各有优缺点和适用场景:
🧩 1. 同步双写 (Synchronous Dual Write)
-
操作顺序:
-
先更新数据库,再更新缓存。
-
先删除缓存,再更新数据库。
-
-
特点:
-
强一致性要求高: 尽力保证在本次请求内缓存和数据库最终一致(但不绝对保证)。
-
性能开销: 每次写操作需要同步操作 DB + Cache,增加延迟。
-
复杂度: 需要处理失败场景(DB 成功 Cache 失败 / DB 失败 Cache 成功)。
-
策略 | 操作步骤 | 优点 | 缺点 & 风险 | 适用场景 |
---|---|---|---|---|
Write-Through (穿透写) | 1. 更新数据库 2. 立即更新缓存 | 读性能好(缓存命中率高),逻辑简单 | 并发问题: - A 更新 DB -> B 更新 DB -> B 更新 Cache -> A 更新 Cache (缓存中是 A 的旧数据) 失败问题: - DB 成功 Cache 失败:缓存是旧数据 - Cache 成功 DB 失败:缓存是脏数据 | 对一致性要求极高,且写操作不频繁 |
Cache Aside (旁路缓存) | 1. 删除缓存 2. 更新数据库 | 逻辑简单,避免并发写缓存问题 | 缓存击穿: 删除后,大量读请求穿透到 DB 短暂不一致: 删缓存后更新 DB 前,读请求可能加载旧数据到缓存 失败问题: - 删缓存成功,更新 DB 失败:缓存缺失(下次读加载) - 更新 DB 成功,删缓存失败:缓存是旧数据 | 最常用,平衡一致性和性能 |
Write-Behind (后写/回写) | 1. 只更新缓存(标记为脏) 2. 异步批量将脏数据刷回 DB | 写性能极高(延迟低),对 DB 压力小 | 数据丢失风险: 缓存宕机导致未刷盘数据丢失 复杂性高: 需维护脏数据和刷盘逻辑 强一致性差 | 写密集、允许短暂丢失(如点赞、计数) |
📥 2. 异步双写 (Asynchronous Dual Write via MQ)
-
操作顺序:
-
应用成功更新数据库。
-
应用发送一条消息到消息队列(MQ),消息包含更新的数据。
-
独立的消费者服务监听 MQ,消费消息并更新缓存。
-
-
特点:
-
最终一致性: 保证 DB 和 Cache 最终会一致,但存在延迟。
-
性能好: 主流程只操作 DB 和发 MQ,速度快。
-
解耦: 缓存更新逻辑独立于主业务逻辑。
-
可靠性依赖 MQ: 需要保证消息不丢失、至少消费一次。
-
处理幂等性: 消费者更新缓存操作必须是幂等的(因为消息可能重复消费)。
-
适用场景:
-
对强一致性要求不高,接受秒级延迟。
-
写操作非常频繁,同步更新缓存压力过大。
-
系统架构追求解耦。
🔄 缓存双写策略在秒杀系统中的典型应用
在之前设计的秒杀系统中,库存扣减 环节是双写策略的典型应用场景:
-
核心流程(Cache Aside + 异步最终一致):
-
Step 1 (写 Redis): 秒杀服务使用
DECR
或 Lua 脚本原子性地扣减 Redis 中的库存(防止超卖)。 -
Step 2 (写 MQ): 扣减成功后,发送“创建订单”消息到 MQ(不直接写 DB)。
-
Step 3 (写 DB - 订单服务): 订单服务消费 MQ 消息:
-
写数据库 (强一致): 在 DB 中执行
UPDATE stock SET stock = stock - 1 WHERE id=? AND stock > 0
(最终防线,保证强一致不超卖)。 -
创建订单记录。
-
-
Step 4 (异步更新/失效缓存 - 可选):
-
方案 A (监听 DB Binlog): 通过 Canal/Debezium 监听 DB 的库存变更 Binlog,发送消息触发 Redis 库存同步(保持 Redis 与 DB 最终一致)。
-
方案 B (订单服务直接失效): 如果 DB 扣减失败,订单服务发送消息回滚 Redis 库存(
INCR
);成功则通常不需要主动更新 Redis(读请求主要依赖 Redis 预扣的标记,售罄后标记不再变化)。
-
-
-
为什么这样设计?
-
性能: Redis 原子扣减处理超高并发。
-
强一致(核心): DB 的
stock > 0
条件是防止超卖的终极保障。 -
最终一致: Redis 库存与 DB 库存通过异步机制(Binlog 监听或补偿任务)达到最终一致。秒杀期间短暂的 Redis/DB 库存数值差异是可接受的(只要 DB 不超卖)。
-
解耦与泄洪: MQ 将瞬时写 DB 的压力化解为异步处理。
-
📌 总结:缓存双写策略的核心要点
-
目标: 在保证系统性能(利用缓存)的同时,尽力维持缓存与数据库的数据一致性。
-
没有银弹: 每种策略都有其优缺点和适用场景,Cache Aside(删缓存+更新DB) 是实践中最常用、相对平衡的方案。
-
秒杀场景特殊: 秒杀的核心是 Redis 预扣库存(写缓存) + DB 最终强一致扣减(写数据库) + MQ 异步化,是一种特定优化的双写模式。
-
一致性级别: 明确业务对一致性的要求(强一致?最终一致?能容忍多久不一致?)。
-
失败处理: 必须考虑每一步操作失败后的补偿或重试机制(如删缓存失败后重试、通过监听 Binlog 补偿缓存)。
-
监控: 监控缓存与 DB 的一致性延迟、双写失败率等指标至关重要。
选择哪种双写策略,需要根据具体的业务场景、性能要求、数据一致性要求以及团队的技术栈来综合权衡。在秒杀这种极端场景下,通常会采用 Cache Aside 的变种 + 强一致 DB 操作 + 异步最终一致 的组合方案。