RocketMQ 事务消息(半消息)介绍

1. 要解决什么问题

在分布式场景里,「先写数据库再发 MQ 」或「先发 MQ 再写库」单独做,都会在故障时出现只有一种成功、另一种失败的裂缝,例如:

  • 库已更新,但 MQ 没发出去 → 下游永远收不到通知。
  • MQ 已发出,但库回滚或失败 → 下游已动,与本地状态不一致。

事务消息(半消息) 的思路是:先发一条 对消费者不可见(或等价语义)的半消息 ,在 本地事务有明确结论 后,再由 Producer 与 Broker 协作决定 提交(Commit)或丢弃(Rollback) ;若第一次无法定论,则通过 回查(Check) 多次对齐 数据库或其它持久化状态中的事实,再给出终态。


2. RocketMQ Go 客户端:本地事务状态枚举

事务监听器的两个回调方法均返回:

(github.com/apache/rocketmq-client-go/v2/primitive.LocalTransactionState, error)

其中 LocalTransactionState 在依赖库中的定义(节选)为:

go 复制代码
const (
    CommitMessageState   LocalTransactionState = iota + 1 // 1
    RollbackMessageState                                   // 2
    UnknowState                                            // 3(库内拼写为 Unknow)
)
枚举值 数值(按上述定义) 含义
CommitMessageState 1 半消息 提交 :之后对订阅该 Topic 的消费组而言,本条消息按 普通已提交消息 参与投递与消费。
RollbackMessageState 2 半消息 回滚/丢弃 :消费组 不会 收到本条消息(在该事务语义下)。
UnknowState 3 未决 :Broker 侧不结束该条事务消息;由 回查 再次询问,直到变为 Commit/Rollback 或触达业务侧「回查次数上限」等策略。

说明:UnknowState 不会作为消息体字段传给下游消费者 ;它只存在于 Producer 与 Broker 之间的事务收尾协议 中。


3. 两条核心回调:各自干什么、入参出参是什么

实现方在进程内注册 TransactionListener (接口名以所用客户端为准),由 一个监听器类型 承载两个方法即可。

3.1 ExecuteLocalTransaction --- 半消息阶段的「本地事务」

  • 谁调用 :RocketMQ 事务 Producer 所在进程内的客户端,在半消息就绪后 同步(或按客户端实现) 回调。

  • 入参

    • context.Context:超时、取消等。
    • *primitive.Message:半消息。业务上主要使用 msg.Body (常见为 JSON/Protobuf 等)、msg.Topicmsg.GetTags()msg.TransactionId 等。
      注意:TransactionId事务消息 ID ,与 Broker 分配的消费侧 MsgId 不是同一概念;半消息阶段 Message 上通常 没有 与回查阶段 MessageExt.MsgId 对等的完整信息。
  • 出参

    • primitive.LocalTransactionState:三选一 Commit / Rollback / Unknow
    • error:错误信息;最终是否以 error 改变 Broker 决策以客户端实现为准 。常见实践是:需要回查时返回 UnknowState, nil,把「未决」明确体现在第一个返回值上。
  • 实现上通常做什么 :解析 msg.Body → 执行业务约定的 本地副作用 (写库、更新状态机、写发件箱等)→ 返回三态之一。具体是否按 路由字段 (如 process_mode、消息类型枚举等)分支,由项目自行约定。

示例(半消息阶段 --- 通用示意)

示例 A:msg 上与排查相关的字段(示意值)

字段 / 方法 示例值 说明
msg.Topic ORDER_DOMAIN_TOPIC 以实际配置为准
msg.GetTags() PAID 按发送端约定
msg.TransactionId 7F0000012345ABC@...(示意) 事务消息 ID不等于 消费侧 MsgId
msg.Body UTF-8 JSON(或其它序列化)字节 由业务定义反序列化结构

示例 B:msg.Body 的 JSON 骨架(与具体行业无关)

json 复制代码
{
  "meta": { "source": "billing", "occurred_at": 1710000000 },
  "route": { "handler": "default" },
  "payload": {
    "biz_id": "BIZ-2025-0001",
    "amount": 100,
    "currency": "USD"
  }
}
  • route.handler(或等价字段)可为空,表示 默认处理链 ;非空时由实现方 switch/策略表 分发到不同本地逻辑(宣讲时强调「可插拔路由」即可,不必展开各分支业务)。

示例 C:典型返回值(通用语义,不绑定具体函数名)

场景 返回 (LocalTransactionState, error) 对外一句话
本地处理成功且允许投递 (CommitMessageState, nil) 半消息提交,下游将可消费
消息体非法、鉴权失败等 确定不应投递 (RollbackMessageState, err) 半消息丢弃
本地处理失败且 无法确定是否已部分落库 (UnknowState, nil) 未决,交给回查读持久化状态再定
基础设施瞬时故障、策略上选择未决 (UnknowState, err 或 nil) 依项目约定;宣讲点:未决 ≠ 下游可见
业务规则明确 禁止再投递(如终态冲突) (RollbackMessageState, nil) 不提交半消息

3.2 CheckLocalTransaction --- 事务回查

  • 谁调用 :当 ExecuteLocalTransaction 返回 UnknowState ,或 Broker/客户端认为需要确认时,之后、可多次 调用。

  • 入参

    • context.Context
    • *primitive.MessageExt:在 Message 基础上扩展,包含 MsgId回查次数 (通过 GetProperty(primitive.PropertyTranscationCheckTimes) 解析)等,便于日志与限次策略。
  • 出参 :与上相同,仍为 (LocalTransactionState, error)

  • 实现上通常做什么 :根据 msgExt.Body 中的业务主键 查询数据库或缓存 → 若仍未完成可 幂等补跑 → 返回 Commit/Rollback/Unknow;并对 回查次数上限 做保护(超限 Rollback + 告警),避免无限悬挂。

示例(回查阶段 --- 与半消息阶段对照)

示例 A:msgExt 比半消息阶段多了什么(示意)

字段 / 方法 示例值 说明
msgExt.Body 与半消息 同一条业务载荷 路由规则与第一次一致
msgExt.MsgId 0A1B2C3D4E5F6789ABCDEF0000(示意) Broker 侧消息标识
msgExt.TransactionId 与半消息阶段 相同事务 ID(示意) 串联同一条事务消息生命周期
msgExt.GetProperty(PropertyTranscationCheckTimes) "1""2"、... 第几次回查;与配置 maxCheckTimes 比较

说明:依赖库中属性名为 PropertyTranscationCheckTimes (拼写为 Transcation,与 RocketMQ 历史命名一致)。

示例 B:典型返回值(通用语义)

场景 返回 (LocalTransactionState, error) 对外一句话
持久化状态已表明 可投递 (CommitMessageState, nil) 提交半消息
幂等补跑后成功 (CommitMessageState, nil) 回查阶段收口为可投递
持久化状态或策略表明 不可投递 (RollbackMessageState, nil) 丢弃半消息
回查次数 超过上限 (RollbackMessageState, nil) 防止无限 Unknown,并应 告警
仍无法读库或仍 transient (UnknowState, nil) 继续未决,等待下次回查

4. 代码如何拆(职责分层 --- 宣讲用表)

下表描述 常见拆分方式 ,便于听众理解「不是两个巨型函数写完一切」;具体文件名、函数名为实现细节,各团队可不同。

分层 / 模块 对外一句话 对内说明(不含具体业务算法)
监听器入口 实现 TransactionListener,注册到 MQ 客户端。 对外暴露 ExecuteLocalTransaction / CheckLocalTransaction;内部可 薄封装,转发到 execute/check 两个模块。
半消息执行(execute) 半消息 第一次到达 时跑本地逻辑。 解析 msg.Body路由 、调用各领域 用例/服务;返回 Commit/Rollback/Unknow。
事务回查(check) 未决 时反复询问,直到可判定。 解析回查次数、限次 、读持久化状态、可选 幂等重试;返回三态。
领域分支(可选) 按路由走不同本地事务。 独立文件/包均可,只要从 execute/check 统一入口 分发,避免回调内无限膨胀。
消息契约(可选) 信封、路由常量、序列化辅助。 MQ 契约版本 对齐;路由字段建议 显式、可测

5. 「HTTP 入站」与「事务监听器」不要混为一谈(通用对照)

通道 典型形态 说明
外部系统 → 本服务 HTTP 渠道/合作方回调、开放平台通知 入参为 HTTP;与 MQ 事务 无直接同一调用栈
本服务 → RocketMQ 事务发送 应用内异步可靠扩散 已整理的业务载荷 写入 msg.Body,再走 半消息 → Execute →(必要时)Check → Commit/Rollback
消费组 Push/Pull 消费者 仅在 Commit 之后 收到消息;须 幂等 处理。

宣讲可强调:事务监听器处理的是「已进入本进程、并放在 msg.Body 里的那条载荷」,而不是 HTTP 连接本身。


6. 死信队列(DLQ)在整条链路中的位置

  • 半消息 + ExecuteLocalTransaction + CheckLocalTransaction :解决的是 「这条消息要不要提交给消费方」不属于 「消息已被消费但一直失败」的场景,因此 通常不称为 DLQ 问题
  • RollbackMessageState :半消息丢弃,无下游消费无消费侧 DLQ
  • UnknowState + 回查 :仍是 事务未决收尾 ;回查次数超限时常见策略为 Rollback + 告警/运维介入 ,仍 不是 消费 DLQ。
  • Commit 之后 :消息进入与普通消息相同的投递路径;若 Push 消费者 处理失败,会按 重试队列 / 最大重消费次数 等配置重试;超过上限 时,RocketMQ 常见行为是进入 死信队列(Topic 名常带 %DLQ% + ConsumerGroup) 或等价隔离,需运维/补偿程序处理。

7. 端到端流程图(宣讲用)

7.1 事务生产者侧(半消息 + 执行 + 回查)

flowchart TD subgraph Up["上游应用内"] U1["业务完成"] U2["构造 msg.Body"] U3["事务发送 API"] U1 --> U2 --> U3 end subgraph Brk["Broker"] B1["半消息 prepared"] B2{"终态"} B3["Commit 后可投递"] B4["Rollback 丢弃"] B1 --> B2 B2 -->|Commit| B3 B2 -->|Rollback| B4 end subgraph Lst["TransactionListener"] E["ExecuteLocalTransaction
解析 Body、本地处理"] C["CheckLocalTransaction
读状态、限次、补判"] E -->|Commit| B2 E -->|Rollback| B2 E -->|Unknow| C C -->|再判| B2 end U3 --> B1 B1 --> E

说明:图中 Unknow 与 Go 依赖库常量拼写一致(UnknowState)。

7.2 枚举与消费侧、DLQ 的关系(简图)

flowchart LR subgraph Tx["LocalTransactionState"] S1["1 Commit"] S2["2 Rollback"] S3["3 Unknow"] end subgraph Post["Commit 之后"] M["消费"] R{"成功?"} RT["重试"] DLQ["DLQ"] M --> R R -->|是| OK["ACK"] R -->|否| RT --> M RT -->|超次| DLQ end S1 --> M S2 --> X["无消费"] S3 --> CK["Check"] CK --> S1 CK --> S2

说明:DLQ 节点表示「超过最大重试等策略后可能进入死信」;标签内避免未闭合的 :/" 混用,以免部分渲染器解析失败。


8. 一句话总结

先发半消息占位;ExecuteLocalTransaction 完成与消息绑定的本地处理并返回 Commit(1) / Rollback(2) / Unknow(3);若为 Unknow,则由 CheckLocalTransaction 结合持久化状态多次补判;只有 Commit 后下游消费者才收到同一条载荷;消费反复失败才进入与事务无关的 DLQ 讨论。


相关推荐
小堃学编程1 天前
【项目实战】基于protobuf的发布订阅式消息队列(4)—— 服务端
c语言·c++·vscode·消息队列·gtest·protobuf·muduo
AutoMQ2 天前
AWS 新发布的 S3 Files 适合作为 Kafka 的存储吗?
云原生·消息队列·云计算
何中应4 天前
在windows本地部署RabbitMQ
分布式·消息队列·rabbitmq
AutoMQ6 天前
别再每月浪费数千美元:拆解 AWS/GCP Kafka 背后的隐性账单
kafka·消息队列·aws
Micro麦可乐7 天前
Redis只会用来做缓存?解锁Redis非缓存的九个应用场景,90%程序员不知道的隐藏技能
数据库·redis·缓存·消息队列·分布式锁·延迟队列·布隆过滤器
恋喵大鲤鱼14 天前
分布式消息投递模型快速上手
消息队列·投递模型
少许极端16 天前
消息队列5-RabbitMQ的高级特性和MQ的应用问题与解决方案-事务、消息分发的应用、幂等性保证、顺序性保证、消息积压的解决
分布式·消息队列·rabbitmq
却话巴山夜雨时i17 天前
互联网大厂Java面试场景:从基础到微服务的循序渐进提问
java·数据库·spring·微服务·面试·消息队列·技术栈
__土块__17 天前
一次支付清结算系统线程池故障复盘:从任务积压到异步解耦的架构演进
java·消息队列·rocketmq·线程池·支付系统·故障复盘·异步架构