什么是双写一致性问题?如何解决双写失败?
简化版
双写一致性问题是指一次业务操作要写两个资源,例如数据库和 MQ、数据库和缓存、两个服务数据库,其中一个成功另一个失败会造成不一致。解决方式包括本地事务加 Outbox、事务消息、可靠重试、补偿、对账,以及尽量避免同步双写。
详细版
双写常见场景:
- 写数据库后发 MQ。
- 更新数据库后删除缓存。
- 订单服务写订单,同时库存服务扣库存。
- 写 MySQL 后同步 Elasticsearch。
解决思路:
| 场景 | 常见方案 |
|---|---|
| DB + MQ | Outbox、本地消息表、事务消息 |
| DB + Cache | 更新 DB 后删除缓存,失败重试 |
| 跨服务写 | Saga、TCC、可靠消息、补偿 |
| DB + ES | CDC、MQ 异步同步、对账重建 |
面试重点:双写不能只靠 try-catch,必须设计失败后的恢复路径和幂等机制。
完整版教学
一、双写问题为什么难
双写难在两个资源通常不属于同一个事务管理器。
例如:
更新订单数据库成功
发送订单消息失败
或者:
发送消息成功
更新数据库失败
只要中间有网络、进程、下游系统,就可能出现半成功状态。分布式系统不能假设两个动作永远一起成功。
二、DB 和 MQ 双写怎么处理
DB 和 MQ 双写最常用的是 Outbox 或事务消息。
Outbox 把业务数据和消息事件放进同一个本地事务,后续异步投递。事务消息则利用 MQ 的半消息和事务回查能力,让消息状态和本地事务结果关联。
两者目标都是避免:
业务成功但消息丢
消息发出但业务失败
三、DB 和缓存双写怎么处理
数据库和缓存双写不建议“更新数据库后直接更新缓存”,因为并发下旧值可能覆盖新值。
常用方案是更新数据库成功后删除缓存。删除失败要重试,缓存本身设置 TTL 兜底。对一致性要求更高的场景,可以使用延迟双删或 binlog 监听补删。
这类方案本质是最终一致:允许短时间旧缓存,但要保证旧缓存不会永久存在。
四、跨服务双写怎么处理
跨服务写更复杂。比如下单要创建订单和扣库存。
如果强一致要求很高,可以用 TCC:库存 Try 冻结,订单 Confirm 创建成功,失败 Cancel 释放库存。
如果可以最终一致,可以先创建订单,再发送库存扣减事件;库存失败后订单进入异常或取消补偿。
选择方案时要看业务能否接受中间状态。不是所有跨服务写都要 TCC,TCC 成本很高。
五、双写失败后的恢复路径
任何双写方案都要回答:
第一个写成功,第二个写失败怎么办?
第二个其实成功了,但响应超时怎么办?
重复重试会不会产生副作用?
长期不一致怎么发现?
答案通常是重试、幂等、补偿和对账组合,而不是某一个技术点。
六、如何从架构上减少双写
双写问题最好不是事后修,而是在建模时减少。比如一个服务拥有某份数据的写入权,其他系统通过事件订阅构建只读视图,而不是多个服务同时写同一份事实。
如果只是为了查询方便而双写,可以考虑读模型、宽表、缓存或搜索索引,这些都应被视为可重建的派生数据。派生数据不应该反过来成为权威事实。
如果两个写入都非常关键,要先判断是否真的属于同一个强一致边界。能放进同一个本地事务的,不要过早拆成跨服务双写;必须跨服务的,再考虑 TCC、Saga、Outbox 或事务消息。
七、常见误区与追问
这道题要紧扣「双写一致性问题」本身回答,不能把它混成泛泛的数据一致性套话。面试官通常会追问“写入怎么确认、失败怎么补、旧数据怎么防、成本在哪里”,所以回答要覆盖一致性目标、写入顺序、消息投递、幂等、补偿、对账和读写体验。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 双写问题是一次业务同时更新两个资源时无法保证两边原子成功,常见于数据库加缓存、数据库加 MQ | 不要停在名词解释 |
| 流程机制 | 发起业务写入 -> 更新第一个资源 -> 更新第二个资源 -> 任一步失败产生不一致 -> 重试或补偿修复 -> 对账确认收敛 | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 写库成功后发 MQ 失败,订单已支付但下游库存不知道;发 MQ 成功后写库回滚,下游又收到假事件 | 一致性方案本质是在强一致成本、系统可用性、延迟和业务可接受的不一致窗口之间取舍 |
双写一致性问题 面试拆解:
1. 发起业务写入
2. 更新第一个资源
3. 更新第二个资源
4. 任一步失败产生不一致
5. 重试或补偿修复
6. 对账确认收敛
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「双写一致性问题」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:先写 A 再写 B 就能保证一致。 两步之间任何崩溃、超时或网络失败都会留下不一致窗口。
- 误区:把顺序反过来就彻底解决。 先发消息还是先写库都有另一类失败窗口。
- 误区:重试一定能解决双写。 重试要有幂等和可观测,否则可能重复副作用或无限重试。
- 追问:数据库和 MQ 双写怎么治理? 本地消息表、Outbox、事务消息、CDC 或可靠消息最终一致。
- 追问:缓存双写常用什么? 先更新数据库再删除缓存,配合重试、TTL、延迟双删和 binlog 兜底。
- 追问:强一致能不能做? 可以用分布式事务,但成本、可用性和性能代价更高。
八、加强记忆
双写一致性问题的本质是“两个资源不能被一个普通本地事务同时保护”。解决它要么把其中一个写入变成本地可靠事件,要么使用事务消息/TCC/Saga,要么接受最终一致并配套重试、幂等、补偿和对账。