什么是 Outbox Pattern?它如何解决本地事务和消息发送一致性?
简化版
Outbox Pattern 是在本地事务中同时写业务数据和消息事件表,事务提交后再异步把事件发送到消息队列。它解决的是“数据库更新成功但消息发送失败”或“消息发送成功但事务回滚”导致的数据不一致问题。
详细版
普通做法中,业务更新和发消息是两个动作:
- 更新订单状态。
- 发送订单已支付消息。
如果更新成功后应用宕机,消息就丢了;如果先发消息再提交事务,事务回滚后下游却收到假消息。
Outbox 的做法是:
- 在同一个本地事务内更新业务表。
- 在同一个本地事务内插入 outbox 事件表。
- 后台投递器扫描 outbox 表,发送 MQ。
- 发送成功后标记事件已发送。
- 发送失败继续重试。
它不能保证下游只消费一次,所以消费者仍要做幂等。
完整版教学
一、为什么发消息和写数据库会不一致
分布式系统里,写数据库和发 MQ 通常不是同一个事务资源。你很难用一个普通本地事务同时包住它们。
如果先写数据库再发消息:
更新订单成功
应用宕机
消息没发出去
下游库存、积分、通知服务就不知道订单变化。
如果先发消息再写数据库:
消息发出去了
数据库事务失败回滚
下游以为订单已支付,但订单库里并没有成功状态。这也很危险。
二、Outbox 的核心思想
Outbox 把“要发的消息”先当成本地数据保存下来。
在订单服务本地事务中:
update orders set status = 'PAID' where id = ?;
insert into outbox_event(event_id, event_type, payload, status)
values (?, 'OrderPaid', ?, 'NEW');
这两个操作由同一个数据库事务保证,要么都成功,要么都失败。
事务提交后,消息是否立刻发出去已经不是最关键的,因为 outbox 表里有可靠记录,可以稍后重试。
三、Outbox 投递器怎么工作
投递器可以是后台线程、定时任务或独立服务。它不断扫描 NEW 状态事件,发送到 MQ,成功后把状态改为 SENT。
伪流程:
select NEW events limit 100
for each event:
send MQ
if success: mark SENT
else: increase retry_count
为避免多个投递器重复抢同一事件,可以使用状态抢占、乐观锁、数据库锁或分片扫描。
四、Outbox 的重复消息问题
Outbox 常见语义是至少一次投递。比如 MQ 已经收到消息,但投递器更新事件状态时失败,那么下次扫描会再次发送同一事件。
所以消费者必须幂等。可以用 event_id 或业务 ID 建消费记录:
consumer_name + event_id 唯一
插入成功才处理,插入失败说明已处理过。
五、Outbox 和事务消息的关系
有些 MQ 提供事务消息能力,例如先发送半消息,再执行本地事务,最后提交或回滚消息。Outbox 则更通用,不强依赖 MQ 的事务能力。
Outbox 的优点是可控、可追踪、便于补偿;缺点是要维护事件表、投递任务、重试和归档。
六、常见误区与追问
这道题要紧扣「Outbox 模式」本身回答,不能把它混成泛泛的数据一致性套话。面试官通常会追问“写入怎么确认、失败怎么补、旧数据怎么防、成本在哪里”,所以回答要覆盖一致性目标、写入顺序、消息投递、幂等、补偿、对账和读写体验。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | Outbox 把业务数据和待发送事件写入同一个本地事务,再由后台可靠投递事件,解决数据库和 MQ 双写窗口 | 不要停在名词解释 |
| 流程机制 | 业务事务写订单 -> 同事务写 Outbox 事件 -> 后台扫描未发送事件 -> 投递到 MQ -> 标记已发送 -> 消费者幂等处理 | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 订单表写入和 outbox_event 写入同一事务成功后,即使进程崩溃,后台任务仍可扫描事件表补发消息 | 一致性方案本质是在强一致成本、系统可用性、延迟和业务可接受的不一致窗口之间取舍 |
Outbox 模式 面试拆解:
1. 业务事务写订单
2. 同事务写 Outbox 事件
3. 后台扫描未发送事件
4. 投递到 MQ
5. 标记已发送
6. 消费者幂等处理
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「Outbox 模式」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:Outbox 能让 MQ 和数据库强一致。 它保证本地业务状态和待发送事件原子,MQ 投递仍是最终一致。
- 误区:事件表可以无限增长。 需要归档、清理、分区和投递状态管理。
- 误区:Outbox 不需要消费者幂等。 投递重试可能产生重复消息,消费者仍要幂等。
- 追问:Outbox 解决什么问题? 解决业务写库成功但发消息失败的双写窗口。
- 追问:后台投递失败怎么办? 按状态重试、指数退避、告警和人工处理。
- 追问:Outbox 和 CDC 怎么结合? 可以让 CDC 订阅 Outbox 表变更并投递,减少业务进程发消息责任。
七、加强记忆
Outbox 的记忆点是“业务数据和消息记录同事务,真正发消息异步重试”。它不追求一次性把数据库和 MQ 变成强事务,而是先保证事件不丢,再靠投递重试和消费幂等实现最终一致。