跳转到内容

本地消息表与 Outbox 模式

用途:解决“数据库事务提交了,消息却没发出去”这类经典不一致时查阅

跨资源写入有两个动作:更新本地数据库、向消息中间件发送事件。二者不在一个事务里,于是存在四种失败组合,其中两种会造成永久不一致:

顺序 故障 后果
先发消息后提交 提交失败 下游收到未发生的事件(脏事件)
先提交后发消息 发送失败/进程崩溃 事件永久丢失,两侧长期不一致

Outbox(发件箱)模式把两个动作合并为一个本地事务:事件与业务数据一起写入同库的 outbox_message 表,由独立投递器把消息搬到中间件并标记完成。

业务表写入 + outbox_message 插入 ← 同一本地事务
↓
投递器(轮询 / CDC)读取未发送记录 → 发送到 MQ → 标记已发送
↓
消费方按业务幂等键去重

关键字段:聚合标识、事件类型、载荷、创建时间、状态(待发送/已发送/失败)、重试次数、下一次重试时间、唯一消息键。

方式 做法 优点 注意点
轮询发布(Poll-Publish) 后台任务定时查表发送 实现简单、无额外组件 延迟取决于轮询周期;多实例需抢占避免重复
CDC 发布 用 Debezium 等监听 outbox 表的 binlog 变更 低延迟、应用无发送代码 需要运维 CDC;顺序与 exactly-once 仍靠消费幂等
  1. 消费端幂等:Outbox 保证“至少一次”,重复投递必然发生。
  2. 顺序要求显式声明:同一聚合的事件需要按序时,用聚合键做分区/顺序消息。
  3. 积压与告警:未发送记录堆积是故障前兆,要有阈值告警。
  4. 清理策略:已发送记录定期归档,避免表膨胀拖慢轮询。
  5. 对账兜底:投递器自身故障时,需要比对两侧状态的修复任务。
方案 一致性 复杂度 适用
直接双写(无 Outbox) 可能永久不一致 低 只适合可容忍丢失的场景
本地消息表 / Outbox 至少一次 + 幂等 中 通用首选,跨库/跨中间件
事务消息(半消息 + 回查) 至少一次 中 中间件原生支持时(RocketMQ)
XA/2PC 强一致 高 参与者少且可控、不可接受不一致

事务消息与 Outbox 解决的是同一个问题,取舍在于:事务消息把可靠性放进中间件(依赖回查实现),Outbox 放进自己的数据库(可审计、可重放、与中间件无关)。

开源参考:Debezium(Outbox Event Router)、Spring Transactional Outbox、Apache RocketMQ 事务消息。

误区 修正
outbox 表与业务表不同库 必须同库同事务,否则问题原样存在
投递器无并发抢占 多实例重复发送,依赖消费端幂等兜住(成本更高)
只发不记状态 无法重放与排障
消费端用消息 ID 去重 业务重试会产生新消息 ID,必须用业务幂等键
忽略载荷版本演进 事件 Schema 需版本化,历史积压消息仍会被消费
  • Chris Richardson《微服务架构设计模式》第 8 章(Outbox 与事务日志钩子)
  • Debezium 官方文档的 Outbox Event Router 章节