RocketMQ 事务消息 vs Outbox 模式:AI 后端的一致性选择
问题
在 之前的文章 里,我写了用 Outbox 模式保证 RAG 数据一致性。核心思路:业务数据和 outbox 记录在同一个数据库事务里写入,然后用 CDC(Canal/Debezium)把 outbox 记录搬到 MQ,消费者再去同步 ES/Doris。
但有一个问题一直被绕开了:
如果你的 MQ 是 RocketMQ,你还需要 Outbox 吗?
RocketMQ 原生支持事务消息——半消息 + 本地事务 + 回查机制。这本身就是”保证本地事务和消息发送原子性”的方案。既然 MQ 原生就能做,为什么还要写一张 outbox 表?
这是做 AI 后端时真实会碰到的选择。两条路都能到终点,但代价不同。
先搞清楚两个方案各自在做什么
Outbox + CDC
1 | 应用 → MySQL 事务 { 业务表 + outbox 表 } → CDC 监听 binlog → 发送到 MQ → 消费者 |
核心思想:把”发消息”变成”写数据库”。 只要业务数据和 outbox 记录在同一个事务里,要么一起成功,要么一起失败。消息的发送由独立的 CDC 组件异步完成,与应用解耦。
RocketMQ 事务消息
1 | 应用 → 发半消息到 MQ(消费者不可见)→ 执行本地事务 → Commit/Rollback 半消息 → 消费者 |
核心思想:先发消息但不让消费者看到,等本地事务确认后再决定是否让消息可见。 如果应用崩了,MQ Broker 会主动回来问”你的事务到底成没成”。
逐维度对比
| 维度 | Outbox + CDC | RocketMQ 事务消息 |
|---|---|---|
| 原子性保证 | 数据库事务(同一 commit) | 半消息 + 回查协议 |
| MQ 绑定 | 不绑定(任意 MQ) | 强绑定 RocketMQ |
| 业务侵入 | 低(写一张 outbox 表) | 中(实现 TransactionListener 回查接口) |
| 额外组件 | 需要 CDC(Canal/Debezium) | 不需要(MQ 原生支持) |
| 延迟 | CDC 延迟(通常 < 1s) | 毫秒级(半消息确认后立即可见) |
| 回查能力 | 无(CDC 不回查,只读 binlog) | 有(Broker 主动回查应用事务状态) |
| 故障恢复 | CDC 断了 → outbox 堆积 → 重连后补发 | 应用崩了 → Broker 回查 → 恢复 |
| 运维复杂度 | 高(多维护 CDC 组件) | 中(只需维护 RocketMQ 集群) |
| 多下游分发 | 天然支持(一条 binlog 发到多个 topic) | 需要应用层做 fan-out |
真正的选择标准不是”哪个更好”
网上很多对比文章会给你一个”选型表格”然后说”小项目用 Outbox,大项目用事务消息”。这种结论没有错,但没用——因为你不是在”选项目方案”,你是在解决一个具体问题。
真正的选择标准是这三个问题:
问题 1:你的下游是单个还是多个?
单个下游:RocketMQ 事务消息更直接。半消息 → 本地事务 → Commit,一气呵成,不需要 CDC 组件。
多个下游:Outbox 更好。一条 outbox 记录通过 CDC 分发到多个 topic,每个下游独立消费。用事务消息的话,你要么在一个事务里发多条半消息(复杂),要么用 fan-out 消费者转发(多一层)。
问题 2:你能不能接受 MQ 选型被锁定?
RocketMQ 事务消息是一个 强绑定 选择。一旦用了,你的消息可靠性逻辑就深度依赖 RocketMQ 的半消息协议。未来如果要迁到 Kafka 或 RabbitMQ,这部分代码全部要重写。
Outbox + CDC 是 MQ 无关 的。outbox 表里存的就是业务事件,用 Canal 还是 Debezium 读、发到 RocketMQ 还是 Kafka,都是可替换的。
如果你的公司已经在用 RocketMQ 且没有迁移计划——事务消息是更轻量的选择。
如果你的 MQ 选型还没定,或者未来可能换——Outbox 更安全。
问题 3:你的延迟要求是什么级别?
Outbox 的延迟取决于 CDC 的延迟。 Canal/Debezium 的正常延迟在 100ms-1s 之间,但在高负载或网络抖动时可能到秒级。如果你的场景是”事务提交后下游必须在 100ms 内感知”,Outbox 可能不够。
RocketMQ 事务消息的延迟是毫秒级。 半消息 Commit 后消费者立即可见,没有 CDC 的中间环节。
但要注意:大多数 AI 后端场景不需要这么低的延迟。 RAG 向量同步、ES 索引更新、Doris 数据导入——这些都是秒级最终一致性就能接受的场景。为了省 500ms 的延迟去绑定一个 MQ 选型,不划算。
AI 后端场景下的具体建议
场景 1:Spring AI + MySQL → ES 向量同步
用户上传文档 → MySQL 存原文 → 生成 embedding → 同步到 ES 做向量检索。
推荐:Outbox + CDC
理由:
- 下游不只是 ES——可能还有 Doris 做分析、DingTalk 做通知
- 延迟要求不高(秒级即可)
- Spring AI 的 embedding 生成本身是异步的,不需要 MQ 事务消息的毫秒级可见
场景 2:Spring AI Agent → 工具调用 → MQ 事件总线
Agent 执行工具调用 → 需要保证”工具执行结果”和”事件发送”的原子性。
推荐:RocketMQ 事务消息(如果已在 RocketMQ 生态)
理由:
- 单一下游(事件总线消费端)
- 需要毫秒级可见(Agent 等待事件反馈)
- Agent 的回查接口天然适配”查本地状态”的语义
场景 3:MySQL → Doris 实时数仓同步
业务数据写入 MySQL → 需要近实时同步到 Doris 做分析。
推荐:Outbox + CDC(或直接 CDC,不需要 Outbox)
理由:
- 这本质上不是”事务+消息”问题,而是数据同步问题
- Canal/Debezium 直接读 binlog 发到 Kafka,Doris 用 Routine Load 消费
- 甚至不需要 outbox 表——直接 CDC 读业务表的变更即可
- 参见 从 Canal 到 Doris 的数据同步链路
一个容易被忽略的问题:回查接口怎么写
RocketMQ 事务消息最容易被忽略的坑是回查接口。
回查的触发场景:应用发完半消息后崩了,没来得及告诉 Broker 是 Commit 还是 Rollback。Broker 等一段时间后主动回来问。
你的回查接口需要做的是:查一下本地事务到底执行了没有。
1 |
|
关键判断:回查接口必须基于 本地数据库的可查询状态 来判断事务是否成功。如果你的事务涉及多个表,回查接口需要检查所有表的状态一致性。这就是为什么事务消息的业务侵入性是”中”而不是”低”——你必须为每个事务场景写一个回查逻辑。
Outbox 模式没有这个问题。outbox 表本身就是事务状态的记录——记录存在 = 事务成功,记录不存在 = 事务失败。CDC 只需要读 binlog,不需要”回查”。
更新已有判断
在 Spring AI + MySQL 用 Outbox 模式保证 RAG 数据一致性 一文中,我的建议是用 Outbox 模式。
需要补充:
Outbox 和 RocketMQ 事务消息不是互斥的。如果你的 MQ 已经是 RocketMQ,对于单一下游、低延迟要求的场景,事务消息是更轻量的选择。对于多下游、MQ 无关的场景,Outbox 仍然是更好的架构。两者解决的是同一个问题的不同侧重点:Outbox 侧重”解耦和可移植”,事务消息侧重”实时性和简洁性”。
这意味着什么
“选哪个”的本质是”你愿意为什么付代价”。 Outbox 的代价是多维护 CDC 组件;事务消息的代价是绑定 RocketMQ。没有免费午餐。
AI 后端的一致性需求通常不苛刻。 RAG 向量同步、ES 索引更新——这些场景秒级最终一致性就够了。不要为了追求毫秒级一致性而引入不必要的复杂度。
如果你的数据同步链路里已经有 Canal/Debezium,Outbox 几乎是”免费”的。 CDC 组件已经在跑了,多读一张 outbox 表的成本可以忽略。这时候专门引入 RocketMQ 事务消息反而是增加复杂度。
事务消息的回查接口是隐藏的技术债。 看起来只是一个方法,但每个事务场景都要写一个,而且必须和业务逻辑保持一致。随着系统演进,回查接口和实际事务逻辑的同步是持续的维护成本。
