← 返回题目列表

Outbox 本地消息表如何设计?它解决什么一致性问题?

困难 第 30 / 33 题 更新于 2026/07/30
Outbox本地消息表一致性

简化版

Outbox 本地消息表把“业务数据变更”和“待发送消息”写在同一个数据库事务里,解决业务提交成功但消息发送失败的问题。后续由后台任务或 CDC 把消息可靠投递出去,下游再用幂等消费保证最终一致。

详细版

直接在业务事务提交后发 MQ,会遇到中间失败:订单创建成功,但发消息超时,库存或积分系统收不到通知。Outbox 的做法是在订单事务里同时插入一条消息记录。

本地消息表常见字段包括 idevent_typeaggregate_idpayloadstatusretry_countnext_retry_atcreated_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 投递怎么选

扫描投递实现简单:后台任务按 statusnext_retry_at 查表。缺点是有轮询延迟和数据库扫描压力。

CDC 投递读取数据库变更日志,例如 binlog,把 outbox 事件推到 MQ。它实时性更好、侵入小,但运维和链路复杂度更高。

小系统可以从扫描开始,大系统再演进到 CDC。无论哪种方式,事件 ID、幂等和补偿都不能省。

六、常见误区与追问

  • 误区:Outbox 能保证消息只发送一次。 它更常见的是至少一次投递,下游必须幂等。
  • 误区:有本地消息表就不需要监控。 积压、重试失败、死信数量都要告警。
  • 误区:payload 随便塞完整对象就行。 事件结构要版本化,避免消费者被字段变化打断。
  • 追问:为什么消息表和业务表要同库事务? 这样才能让业务变更和事件记录原子提交。
  • 追问:发送成功但标记失败怎么办? 允许重复发送,由 event_id 和消费者幂等兜底。

七、加强记忆

记忆钩子:Outbox 像把“要寄的信”先和订单一起放进同一个保险柜;保险柜关上后,邮差可以慢慢寄,但不能让订单成功而信凭空消失。

回答这题要紧扣“本地事务记录事件、异步可靠投递、至少一次、消费者幂等”。如果能补上状态机、重试索引和 CDC 选型,基本就是工程级答案。