Outbox 本地消息表如何设计?它解决什么一致性问题?
简化版
Outbox 本地消息表把“业务数据变更”和“待发送消息”写在同一个数据库事务里,解决业务提交成功但消息发送失败的问题。后续由后台任务或 CDC 把消息可靠投递出去,下游再用幂等消费保证最终一致。
详细版
直接在业务事务提交后发 MQ,会遇到中间失败:订单创建成功,但发消息超时,库存或积分系统收不到通知。Outbox 的做法是在订单事务里同时插入一条消息记录。
本地消息表常见字段包括 id、event_type、aggregate_id、payload、status、retry_count、next_retry_at、created_at。投递程序扫描待发送消息,发送成功后标记完成,失败则重试。
它不是强一致分布式事务,而是最终一致方案。关键配套是唯一事件 ID、状态机、重试退避、死信处理、幂等消费和监控告警。
完整版教学
一、Outbox 解决的核心断点
很多业务需要“写数据库 + 发消息”。比如订单创建后要通知库存、积分、风控。如果先写数据库再发消息,发消息失败会丢事件;如果先发消息再写数据库,数据库回滚后下游可能收到假事件。
Outbox 的核心是让数据库事务只管本地资源:业务表和消息表在同一个库里提交。只要事务成功,消息记录一定存在;只要事务失败,消息记录也不会出现。
BEGIN
INSERT order
INSERT outbox_event
COMMIT
后台投递:outbox_event -> MQ
二、消息表字段怎么设计
消息表要能表达事件身份、业务归属、投递状态和重试计划。
CREATE TABLE outbox_event (
id BIGINT PRIMARY KEY,
event_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(20) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_event_id (event_id),
KEY idx_status_retry (status, next_retry_at)
);
event_id 用于幂等,status + next_retry_at 用于扫描待投递任务,payload 保存事件内容。
三、状态机比布尔字段更稳
只用 sent=true/false 太粗糙。真实系统里会有待发送、发送中、发送成功、发送失败、进入死信等状态。
例如投递程序拿到一批消息后先标记 SENDING,发送成功改 SENT,失败增加 retry 并设置下一次重试时间。多实例扫描时还要防止重复抢同一条。
| 状态 | 含义 | 下一步 |
|---|---|---|
NEW | 新建待投递 | 扫描发送 |
SENDING | 正在发送 | 成功或失败 |
SENT | 已发送 | 归档 |
DEAD | 多次失败 | 人工处理 |
四、重复发送为什么不可避免
Outbox 通常只能做到至少一次投递。发送 MQ 成功后,如果更新消息表状态失败,后台任务下次会再次发送同一事件。
所以下游必须幂等。可以用 event_id 建消费去重表,或者让业务更新具备唯一约束和状态判断。
数字例子:发送成功后状态更新失败概率即使只有 0.1%,每天 100 万条事件也可能产生 1000 条重复投递。没有幂等消费一定会出事故。
五、扫描投递和 CDC 投递怎么选
扫描投递实现简单:后台任务按 status 和 next_retry_at 查表。缺点是有轮询延迟和数据库扫描压力。
CDC 投递读取数据库变更日志,例如 binlog,把 outbox 事件推到 MQ。它实时性更好、侵入小,但运维和链路复杂度更高。
小系统可以从扫描开始,大系统再演进到 CDC。无论哪种方式,事件 ID、幂等和补偿都不能省。
六、常见误区与追问
- 误区:Outbox 能保证消息只发送一次。 它更常见的是至少一次投递,下游必须幂等。
- 误区:有本地消息表就不需要监控。 积压、重试失败、死信数量都要告警。
- 误区:payload 随便塞完整对象就行。 事件结构要版本化,避免消费者被字段变化打断。
- 追问:为什么消息表和业务表要同库事务? 这样才能让业务变更和事件记录原子提交。
- 追问:发送成功但标记失败怎么办? 允许重复发送,由 event_id 和消费者幂等兜底。
七、加强记忆
记忆钩子:Outbox 像把“要寄的信”先和订单一起放进同一个保险柜;保险柜关上后,邮差可以慢慢寄,但不能让订单成功而信凭空消失。
回答这题要紧扣“本地事务记录事件、异步可靠投递、至少一次、消费者幂等”。如果能补上状态机、重试索引和 CDC 选型,基本就是工程级答案。