双写一致性是高并发分布式系统的核心痛点 ,指当缓存(如 Redis)和数据库同时存储同一份数据时,对数据进行更新操作后,两者的数据保持一致的特性。
在高并发场景下,缓存是缓解数据库压力的关键,但双写操作的顺序、并发竞争 会直接导致数据不一致,进而引发脏读、旧数据覆盖新数据等问题。以下内容从核心矛盾、常见策略、解决方案、面试考点四个维度展开,覆盖生产落地全方案。
一、 双写一致性的核心矛盾
双写一致性的问题根源在于 「操作顺序」 和 「并发竞争」,我们先通过两个典型场景理解不一致的产生过程:
场景 1:并发更新导致的脏数据
假设存在两个请求 A 和 B,同时更新同一数据(商品 ID=1 的库存),采用 「先更新数据库,再更新缓存」 策略:
- 请求 A:更新数据库,库存从 100→99;
- 请求 B:更新数据库,库存从 99→98;
- 请求 B:更新缓存,缓存库存 = 98;
- 请求 A:更新缓存,缓存库存 = 99;最终结果:数据库库存 = 98,缓存库存 = 99 → 数据不一致,后续请求会读取到缓存中的旧数据。
场景 2:并发读写导致的缓存穿透 + 旧数据回写
采用 「先删除缓存,再更新数据库」 策略,存在请求 A(更新)和请求 B(查询)并发执行:
- 请求 A:删除缓存中商品 ID=1 的库存数据;
- 请求 B:查询商品 ID=1 的库存,缓存未命中,从数据库读取旧数据(库存 = 100);
- 请求 B:将旧数据写入缓存;
- 请求 A:更新数据库,库存从 100→99;最终结果:数据库库存 = 99,缓存库存 = 100 → 数据不一致,且短时间内无法自动恢复。
二、 三种经典双写策略对比(优缺点 + 适用场景)
双写一致性的核心是选择合理的操作顺序,以下是三种主流策略的详细对比,这是面试的核心考点:
| 策略 | 操作流程 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 先更新数据库,再更新缓存 | 1. UPDATE db SET ... 2. SET cache KEY VALUE |
实现简单,无需额外操作 | 1. 并发更新导致脏数据;2. 写多读少场景下,缓存更新无效(更新后无请求读取),浪费资源 | 低并发、写少读多、对一致性要求低的场景 |
| 先删除缓存,再更新数据库 | 1. DEL cache KEY 2. UPDATE db SET ... |
避免缓存更新的无效写入 | 1. 并发读写导致旧数据回写;2. 短暂缓存穿透(删除后更新前,请求直接查库) | 写多读少、对一致性要求中等的场景 |
| 先更新数据库,再删除缓存 | 1. UPDATE db SET ... 2. DEL cache KEY |
1. 并发问题影响最小;2. 无效缓存更新少 | 1. 存在短暂的缓存不一致(更新后删除前,缓存是旧数据);2. 删除失败会导致缓存脏数据 | 高并发场景首选,生产环境主流方案 |
✅ 核心结论:为什么「先更新数据库,再删除缓存」是最优解?
- 并发影响最小:即使并发更新,最终缓存会被删除,后续请求会从数据库加载最新数据;
- 无效操作少:只有当数据被查询时,缓存才会被重新加载,避免写多读少场景下的缓存无效更新;
- 修复成本低:若删除缓存失败,可通过重试、定时任务兜底,比其他策略更容易恢复一致性。
三、 生产级双写一致性解决方案(按优先级排序,层层兜底)
选择「先更新数据库,再删除缓存」的基础上,需通过额外机制 解决残留的一致性问题,以下方案按 「实现成本从低到高、保障强度从弱到强」 排序,组合使用可覆盖 100% 场景。
🔥 方案 1:延迟双删(解决并发读写导致的脏数据,必做)
这是最简单、成本最低 的解决方案,针对「先删缓存再更库」或「先更库再删缓存」的并发问题,核心是通过延迟第二次删除,清理并发写入的旧数据。
实现步骤(以「先更库再删缓存」为例)
- 执行
UPDATE db SET ...→ 更新数据库; - 执行
DEL cache KEY→ 第一次删除缓存; - 休眠 N 毫秒(根据业务耗时调整,如 500ms~1s);
- 执行
DEL cache KEY→ 第二次删除缓存。
核心原理
- 休眠的 N 毫秒,是为了等待并发的读请求完成「缓存未命中→查库→写缓存」的全过程;
- 第二次删除,会将并发读请求写入的旧数据删除,后续请求会从数据库加载最新数据。
关键配置
- 休眠时间:需大于一次数据库查询 + 缓存写入的耗时 ,生产环境建议通过压测确定,一般设置为 500ms~2s;
- 实现方式:可通过
Thread.sleep()实现,或用异步线程执行第二次删除,避免阻塞主线程。
🔥 方案 2:分布式锁(保证双写操作的原子性,高并发核心)
在高并发场景下,延迟双删仍可能存在极端情况的不一致,此时需通过分布式锁保证「数据库更新 + 缓存删除」的原子性,避免并发竞争。
实现步骤
- 请求到来时,先通过 Redis SETNX 或 Redisson 获取分布式锁,锁的 Key 为业务唯一标识(如
lock:goods:1); - 获取锁成功后,执行
UPDATE db SET ...→ 更新数据库; - 执行
DEL cache KEY→ 删除缓存; - 释放分布式锁;
- 获取锁失败的请求,等待重试或直接返回。
核心原理
- 分布式锁保证同一时间只有一个请求能执行双写操作,彻底杜绝并发竞争导致的不一致;
- 锁的过期时间需大于业务操作耗时,避免死锁(推荐用 Redisson 的自动续期锁)。
适用场景
- 秒杀、库存扣减等超高并发写场景,对数据一致性要求极高的核心业务。
🔥 方案 3:版本号机制(避免旧数据覆盖新数据,进阶方案)
通过给数据添加版本号,在更新缓存时校验版本号,确保只有最新版本的数据能写入缓存,解决并发更新导致的脏数据问题。
实现步骤
- 数据库表中新增
version字段,初始值为 0,每次更新时version +1; - 更新数据库:
UPDATE db SET ..., version = version +1 WHERE id = 1; - 查询数据库获取最新
version(如version=5); - 删除缓存时,将版本号写入缓存(如
SET cache:goods:1:version 5); - 后续读请求从数据库加载数据时,需校验版本号:只有当数据库版本号 > 缓存版本号时,才写入缓存。
核心原理
- 版本号是数据的「时间戳」,确保只有最新版本的数据能覆盖缓存,避免旧数据回写。
🔥 方案 4:基于 Canal 的异步更新(高并发终极方案,无侵入)
以上方案均为同步操作 ,在超高并发场景下会影响接口性能,此时推荐使用 Canal 监听数据库 binlog ,异步更新缓存,实现非侵入式的双写一致性。
核心原理
- Canal 伪装成 MySQL 的从库,实时监听数据库的 binlog 日志;
- 当数据库发生更新操作时,Canal 捕获到 binlog 事件;
- Canal 将更新事件发送到消息队列(如 RocketMQ/Kafka);
- 消费端监听消息队列,根据 binlog 内容异步删除或更新缓存。
优势
- 非侵入式:无需修改业务代码,对业务无感知;
- 高性能:异步操作不阻塞主线程,适合高并发场景;
- 强一致性:基于 binlog 保证数据更新的最终一致性,支持跨服务、跨机房的缓存同步。
生产最佳实践
- 结合「先更库再删缓存」的同步策略 + Canal 异步兜底:同步删除保证大部分场景的一致性,Canal 捕获遗漏的更新事件,确保最终一致性;
- 适合大规模分布式系统,如电商、金融的核心业务。
🔥 方案 5:定时任务兜底(最终一致性保障,必做)
以上方案仍可能存在极端情况的不一致(如缓存删除失败、Canal 宕机),此时需通过定时任务做最终兜底,实现「数据对账 + 修复」。
实现步骤
- 编写定时任务(如每 5 分钟执行一次),批量查询数据库中的核心数据(如商品库存、订单状态);
- 对比缓存中的数据与数据库数据,若不一致,则以数据库数据为准,更新缓存;
- 记录不一致的数据日志,方便排查问题。
核心原则
- 定时任务是最终兜底手段,不能替代前面的策略,只能作为补充;
- 任务执行频率根据业务一致性要求调整,核心业务可设置为 1~5 分钟 ,非核心业务可设置为 30 分钟~1 小时。
四、 面试高频问题 & 标准答案
1. 为什么不推荐「更新缓存」,而是推荐「删除缓存」?
答:
- 避免并发脏数据:更新缓存会导致并发请求覆盖,删除缓存则让后续请求从数据库加载最新数据;
- 减少无效操作:写多读少场景下,更新缓存后可能无请求读取,造成资源浪费;
- 实现简单:删除缓存的逻辑比更新缓存更简单,无需关心缓存的序列化 / 反序列化。
2. 延迟双删的原理是什么?休眠时间如何确定?
答:
- 原理:通过两次删除缓存,第一次删除是正常操作,第二次删除是为了清理并发读请求写入的旧数据,保证后续请求读取最新数据;
- 休眠时间确定 :需大于「一次数据库查询 + 缓存写入的耗时」,生产环境通过压测确定,一般设置为 500ms~2s,避免过长阻塞主线程。
3. 双写一致性的最终解决方案是什么?
答 :生产环境推荐 「同步策略 + 异步兜底 + 定时对账」 的组合方案:
- 同步策略:先更新数据库,再删除缓存 + 分布式锁,保证高并发下的一致性;
- 异步兜底:基于 Canal 监听 binlog,异步更新缓存,非侵入式;
- 定时对账:定时任务对比缓存和数据库数据,修复不一致,实现最终一致性。
4. 缓存穿透、缓存击穿、缓存雪崩和双写一致性的关系?
答:
- 双写一致性问题可能引发缓存穿透(如先删缓存再更库时,并发读直接查库)、缓存击穿(如热点数据的缓存被删除后,大量请求查库);
- 解决双写一致性的过程中,也能缓解这些问题(如延迟双删减少旧数据回写,分布式锁减少并发查库)。
五、 生产最佳实践总结
- 低并发场景:先更新数据库,再删除缓存 + 延迟双删,成本最低;
- 高并发写场景:分布式锁 + 先更库再删缓存,保证原子性;
- 大规模分布式场景:Canal 异步更新 + 定时任务兜底,非侵入式,高性能;
- 核心铁律 :永远不要让缓存的更新逻辑依赖于业务代码,尽量通过异步、兜底机制保证一致性,减少业务耦合。