当前位置: 首页 > news >正文

深入 RocketMQ 内核:事务消息——分布式事务的终极解法(四)

说实话,事务消息是 RocketMQ 最复杂、也最令人惊叹的特性之一。它解决的是一道难题——在分布式系统中,如何保证“发送消息”和“执行本地事务”要么一起成功,要么一起失败?

今天这篇文章,我们就来彻底搞懂事务消息。我会从它要解决的问题开始,一步步深入到半消息、事务回查、状态机等核心机制。老规矩,配合流程图,一步一图。

八、事务消息
事务消息诞生的背景与要解决的问题
我们先来看一个经典的分布式事务困境。

你在电商系统里做一个“下单扣库存”的业务。订单服务和库存服务是两个独立的服务,通过消息来解耦:

分布式事务困境

订单服务创建订单

发送扣库存消息

消息发送成功
但订单插入失败?

⚠️ 消息已经发出去了
库存被扣了,但订单不存在
→ 数据不一致

订单插入成功
但消息发送失败?

⚠️ 订单创建了
但库存没扣
→ 超卖风险

问题可以归结为一句话:本地数据库操作(订单插入)和消息发送(扣库存通知)这两个操作,不在同一个事务里。它们要么都成功,要么都失败——但在分布式环境下,这很难做到。

常见的解决方案及其局限:

方案 怎么做 问题
先发消息,再执行本地事务 消息发出去后再插入订单 消息发成功但订单插入失败 → 库存被误扣
先执行本地事务,再发消息 插入订单后再发消息 订单插入成功但消息发送失败 → 超卖
本地事务 + 本地消息表 在同一个数据库里存消息表,定时扫描发送 依赖数据库,轮询效率低,有延迟
RocketMQ 事务消息的解法:

RocketMQ 事务消息的核心思想是:让消息的发送和本地事务的执行,通过“半消息 + 事务回查”机制,达成最终一致性。

它不依赖本地消息表,不需要轮询,不需要额外的存储,全部在 Broker 和 Producer 之间协调完成。

事务消息的核心流程:半消息(Half Message)
什么是半消息(Half Message)?

半消息是事务消息的核心概念。它是一种暂不可见的消息——Producer 发送了这条消息,但 Consumer 暂时还消费不到。

半消息

✅ 消息已写入 CommitLog
(有存储)

❌ 消息在 ConsumeQueue 中
被标记为不可见

⏳ 等待本地事务
执行结果

半消息 = 消息已存储,但不可消费
像个“待确认”的订单

半消息的设计意图:

半消息的存在,解决了“消息发送成功了,但本地事务还没执行”这个中间状态的问题。

如果半消息直接可见,Consumer 就能消费到——但此时本地事务可能还没执行完,数据状态不确定
如果半消息不可见,Consumer 就消费不到——等本地事务执行完,再决定是提交还是回滚
这样,消息的可见性和本地事务的执行结果绑定在了一起。

本地事务与半消息的协调机制
事务消息的完整流程,核心就是一个两阶段协调的过程:

成功

失败

COMMIT

ROLLBACK

业务发起事务消息

阶段一:发送半消息

Producer 发送半消息到 Broker

Broker 存储半消息
标记为不可消费

Broker 返回半消息发送成功

阶段二:执行本地事务

Producer 执行本地业务逻辑
(如插入订单)

本地事务结果

阶段三:提交/回滚

Producer 向 Broker
发送事务状态

状态

Broker 将半消息
标记为可见

Consumer 可以消费到消息

Broker 删除半消息
(标记删除)

消息被丢弃,不消费

关键点:

半消息是第一步:先发半消息,再执行本地事务。如果半消息发送失败,直接结束,不用执行本地事务。
本地事务决定最终状态:本地事务成功 → COMMIT;本地事务失败 → ROLLBACK。
Broker 最终执行:COMMIT 使消息可见,ROLLBACK 使消息被丢弃。
事务回查(Check)机制的实现原理
上面我们讨论的是正常情况——本地事务执行完,Producer 能正常告诉 Broker 是 COMMIT 还是 ROLLBACK。

但如果本地事务执行完后,Producer 宕机了,或者网络断了呢?

Broker 怎么知道这条半消息最终应该被提交还是回滚?

答案就是事务回查(Transaction Check)机制。

Broker 扫描半消息

半消息状态
是否为 UNKNOWN?

跳过

Broker 向 Producer
发起事务回查请求

Producer 是否
能正常响应?

标记为需要再次回查
等待下一轮

回查次数
是否超限?

默认回滚
防止消息堆积

Producer 执行
事务状态检查逻辑

返回事务状态
COMMIT / ROLLBACK / UNKNOWN

Broker 根据状态
执行提交或回滚

回查机制的核心设计:

Broker 主动发起:Broker 会定期扫描处于 UNKNOWN 状态的半消息(默认每 60 秒一次)
Producer 被动响应:Producer 需要实现 TransactionListener 的 checkLocalTransaction() 方法,根据业务状态返回最终决定
可配置重试:回查失败会重试,但如果超过配置的重试次数(默认 15 次),Broker 会默认回滚
为什么需要回查?

回查解决了分布式系统中的不确定性问题——当 Producer 不可达时,Broker 不能无限等待,必须有最终决策。回查机制确保了半消息不会永远处于“悬而未决”的状态。

事务消息的状态:COMMIT、ROLLBACK、UNKNOWN
RocketMQ 事务消息有三种状态,由 Producer 的 TransactionListener 返回:

状态 含义 Broker 操作 Consumer 是否可见
COMMIT 本地事务成功,提交消息 将半消息标记为可见 ✅ 可见,可消费
ROLLBACK 本地事务失败,回滚消息 删除半消息 ❌ 不可见,被丢弃
UNKNOWN 本地事务状态未知 暂不操作,等待回查 ❌ 暂不可见,等待决策
public class OrderTransactionListener implements TransactionListener {

@Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { // 执行本地事务:插入订单 orderService.createOrder(arg); return LocalTransactionState.COMMIT_MESSAGE; } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; } } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 回查时,根据业务数据判断事务是否成功 String orderId = msg.getProperty("orderId"); if (orderService.orderExists(orderId)) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.ROLLBACK_MESSAGE; }

}
UNKNOWN 的使用场景:

本地事务执行过程中发生了异常,无法确定是否成功
本地事务执行时间很长,需要异步确认
依赖外部系统的状态,需要等待外部回调
UNKNOWN 状态触发的回查机制,给了系统足够的容错空间。

事务消息的完整时序图与执行流程
把前面所有的内容串起来,我们来看一张完整的时序图:

本地数据库
Consumer
Broker
Producer
业务应用
本地数据库
Consumer
Broker
Producer
业务应用
消息不可见,不被消费
此时不向 Broker 发送最终决定
alt
[多次回查仍返回
UNKNOWN]
loop
[事务回查(默认每 60 秒)]
alt
[本地事务成功]
[本地事务失败]
[本地事务执行异常/超时]

  1. 发送事务消息
  2. 发送半消息
    (状态:PREPARED)
  3. 存储半消息
    (不可消费,记录事务状态)
  4. 半消息发送成功
  5. 回调执行本地事务
  6. 执行本地事务
    (插入订单等)
    7a. 事务提交成功
    8a. 返回 COMMIT
    9a. 提交事务消息
    10a. 标记消息可见
    11a. Consumer 消费到消息
    7b. 事务回滚
    8b. 返回 ROLLBACK
    9b. 回滚事务消息
    10b. 删除半消息
    8c. 返回 UNKNOWN
    9c. 发起回查请求
    10c. 检查本地事务状态
    11c. 查询业务数据
    12c. 返回查询结果
    13c. 返回 COMMIT/ROLLBACK/UNKNOWN
    14c. 提交最终事务状态
    15c. 超过回查次数限制,默认回滚
    流程概括:

发送半消息 → 2. 执行本地事务 → 3. 根据结果决定状态 → 4. COMMIT/ROLLBACK/UNKNOWN → 5. UNKNOWN 触发回查 → 6. 最终确定状态。
这个流程保证了:要么本地事务成功且消息被消费,要么本地事务失败且消息被丢弃。

事务回查的时间参数配置
事务回查涉及多个时间参数,合理配置这些参数是事务消息稳定运行的关键:

参数 默认值 说明 配置位置
transactionTimeout 6000ms 本地事务执行超时时间。超过这个时间,Broker 认为事务状态未知,触发回查 Producer 端
transactionCheckInterval 60000ms Broker 回查半消息的间隔 Broker 端
transactionCheckMax 15 次 最大回查次数。超过后默认回滚 Broker 端

Broker 端配置

transactionCheckInterval=60000 # 回查间隔 60 秒
transactionCheckMax=15 # 最大回查 15 次
transactionCheckTimeout=6000 # 回查请求超时时间 6 秒

Producer 端配置

DefaultMQProducer producer = new DefaultMQProducer();
producer.setTransactionTimeout(6000); // 事务超时 6 秒
配置建议:

transactionTimeout 要根据业务调整:如果本地事务涉及多个数据库操作或远程调用,需要设置足够的超时时间
transactionCheckInterval 不宜过短:频繁回查会增加 Broker 和 Producer 的负载
transactionCheckMax 要结合业务容忍度:核心业务可以设置更大的值(如 30 次),给系统更长的恢复时间
事务消息的使用限制与注意事项
事务消息虽强,但并非万能。在使用时,有几条重要的限制需要牢记:

限制一:不支持延迟消息和批量消息

事务消息不能和延迟消息、批量消息组合使用。事务消息本身就是“先存后发”的机制,和延迟消息的“定时投递”在逻辑上冲突。

限制二:Consumer 必须处理重复消费

事务消息虽然保证了“事务提交后消息可见”,但在异常情况下(如网络重传、Rebalance),Consumer 仍然可能重复消费消息。业务方必须做好幂等处理。

限制三:事务消息的发送是同步的

事务消息只支持同步发送,不支持异步或单向发送。因为需要等待本地事务执行结果,同步是最自然的模式。

限制四:事务消息不支持广播消费

广播消费模式下,每个 Consumer 独立消费,事务消息的状态管理会变得复杂,所以 RocketMQ 的事务消息不支持广播模式。

限制五:半消息对 Broker 有存储开销

每条半消息在 COMMIT 或 ROLLBACK 之前,都会占用 Broker 的存储空间(CommitLog 和 ConsumeQueue)。如果有大量半消息长时间 UNKNOWN,会导致存储膨胀。

使用限制

❌ 不支持延迟消息

❌ 不支持批量消息

❌ 不支持异步/单向发送

❌ 不支持广播消费

⚠️ 半消息占用存储空间

事务消息回查失败的处理策略
回查不是万能的,它也可能失败。我们需要理解各种失败场景及应对策略:

场景一:Producer 节点宕机

如果所有 Producer 节点都宕机了,Broker 的回查请求无法送达。

→ 策略:配置 transactionCheckMax 和 transactionCheckInterval,在 Producer 恢复前,Broker 会持续尝试回查。如果超过最大次数仍无响应,Broker 默认回滚。运维层面需要监控“超时未确认”的半消息数量。

场景二:回查请求超时

Producer 收到了回查请求,但因为网络或负载原因,响应超时。

→ 策略:Broker 会重试回查(下一次扫描时再次发起)。Producer 端应确保 checkLocalTransaction() 方法的执行时间远小于 transactionCheckTimeout。

场景三:checkLocalTransaction 返回 UNKNOWN

Producer 仍然无法确定事务状态(比如依赖的数据库暂时不可用)。

→ 策略:返回 UNKNOWN 后,Broker 会在下一个回查周期再次询问。建议在业务层实现重试机制,直到能确定状态为止。

场景四:超过最大回查次数

15 次回查都失败了,消息仍然 UNKNOWN。

→ 策略:Broker 默认回滚消息(从 5.x 开始,可以通过配置修改默认行为)。运维需要监控这类情况,手动介入处理。

Producer 宕机

响应超时

返回 UNKNOWN

超过最大次数

回查失败

失败类型

等待 Producer 恢复
Broker 持续重试
超限后默认回滚

优化网络
或缩短 checkLocalTransaction 耗时

优化业务检查逻辑
确保能快速确定状态

监控告警
人工介入排查

Done

事务消息与分布式事务的对比(TCC、Saga)
事务消息只是分布式事务的一种解决方案。我们来对比一下主流的几种方案:

TCC(Try-Confirm-Cancel)

维度 TCC 事务消息
核心思想 预留资源 → 确认/取消 半消息 → 提交/回滚
业务侵入 高(需要实现 Try/Confirm/Cancel) 中(只需实现本地事务 + 回查)
数据一致性 强一致性 最终一致性
适用场景 跨服务调用、资源预留 消息驱动的异步场景
性能 较低(多次 RPC) 较高(一次半消息 + 本地事务)
Saga

维度 Saga 事务消息
核心思想 长事务拆分为多个本地事务 + 补偿 本地事务 + 消息最终一致性
业务侵入 高(需要实现正向操作 + 补偿操作) 中
数据一致性 最终一致性 最终一致性
适用场景 长事务、跨多个服务的复杂流程 消息驱动的异步解耦场景
对比总结:

选型建议

分布式事务方案对比

TCC
强一致性
业务侵入高

Saga
最终一致性
需要补偿

事务消息
最终一致性
通过回查自愈

🔵 事务消息:
消息驱动、异步解耦、
对一致性要求不是强同步

🟠 TCC:
跨服务调用、需要强一致性、
资源可预留

🟢 Saga:
长事务、复杂流程、
需要有补偿逻辑

选型建议:

事务消息适合:消息驱动的异步场景,对性能要求高,能接受最终一致性
TCC 适合:跨服务调用的强一致性场景,资源可以预留(如扣减库存、冻结资金)
Saga 适合:复杂的长事务流程,每个步骤都有明确的补偿操作
事务消息与普通消息的性能对比
事务消息因为多了“半消息 + 本地事务 + 可能的回查”这些步骤,性能自然不如普通消息。我们来量化对比一下:

对比维度 普通消息 事务消息 性能差距
网络往返 1 次(发送) 2-3 次(发送半消息 + 提交/回滚 + 可能的回查) 约 2 倍延迟
Broker 写入 1 次 CommitLog 写入 2 次 CommitLog 写入(半消息 + 提交标记) 约 2 倍存储 IO
存储空间 消息体 消息体 + 事务状态记录 约额外 30%-50%
CPU 开销 低 中(需要事务状态管理和扫描) 约 20%-30% 额外
TPS 上限 ~10 万级 ~2-3 万级 约为普通消息的 1/3 - 1/5
事务消息发送

发送半消息

写入半消息

返回 ACK

执行本地事务

发送 COMMIT/ROLLBACK

标记消息状态

返回 ACK

2 次网络往返 + 2 次写入

  • 本地事务 + 可能的回查

普通消息发送

发送请求

写入 CommitLog

返回 ACK

1 次网络往返 + 1 次写入

性能损耗的核心来源:

半消息额外写入:每条事务消息至少多一次 CommitLog 写入
事务状态维护:Broker 需要维护和扫描半消息的状态
网络往返增加:从 1 次变成 2 次(半消息 + 提交/回滚)
回查开销:如果返回 UNKNOWN,还有额外的回查负载
💡 小贴士:如果你的业务不需要强一致性保障,普通消息 + 幂等消费可能是更好的选择。事务消息虽好,但不要滥用——它只应该在“本地事务和消息发送必须保持一致”的场景下使用。

事务消息的内部存储结构

了解事务消息的存储细节,能帮你更深入地理解它的工作原理:

事务消息存储结构

回滚后

标记为已删除
不影响 CommitLog

消息不可消费

提交后

在原 ConsumeQueue 位置
标记为可见

可能增加 Op 消息
记录提交操作

半消息存储

CommitLog 中写入
消息体 + 事务标记

ConsumeQueue 中写入
但标记为不可见

TransactionStateService
记录事务状态 UNKNOWN

关键存储组件:

组件 作用
RMQ_SYS_TRANS_HALF_TOPIC 半消息的存储 Topic,所有事务消息的半消息都暂存于此
TransactionStateService 管理事务状态的服务,扫描半消息并触发回查
Op 消息 记录 COMMIT/ROLLBACK 操作,用于快速判断半消息的最终状态
小结
这篇文章我们彻底吃透了 RocketMQ 的事务消息机制,通过 7 张流程图,搞清楚了:

事务消息要解决的问题——分布式环境下本地事务和消息发送的一致性问题
半消息的概念——暂不可见的中间状态,解决了“先发消息还是先执行事务”的困境
本地事务与半消息的协调——两阶段流程:发送半消息 → 执行本地事务 → 提交/回滚
事务回查机制——当 Producer 不可达时,Broker 主动回查,确保消息不会永远悬而未决
三种事务状态——COMMIT、ROLLBACK、UNKNOWN 的含义和流转
时间参数配置——transactionTimeout、transactionCheckInterval 等参数的作用
使用限制——不支持延迟/批量/异步/广播等
回查失败的处理策略——各种异常场景的应对方案
与其他分布式事务方案的对比——TCC、Saga 的差异和选型建议

http://www.cnnetsun.cn/news/3538364.html

相关文章:

  • 3个理由告诉你为什么VSCode Python扩展是Python开发者的必备神器
  • 2026生鲜零售小程序十大方案测评:库存、配送、自提与会员怎么选?含零代码SAAS、AI编程、源码定制
  • 今天不解决风格漂移,明天就重写全部AI内容——紧急启动AI风格一致性防火墙的5个信号
  • 【AI写作避重黄金法则】:20年技术专家亲授7大原创性强化技巧,查重率直降92%
  • 如何永久保存微信聊天记忆:从数据提取到年度报告完整教程
  • uniapp.4
  • 终极企业级AI可视化编辑器:Onlook完整实战指南
  • WPS AI排版避坑清单:这8个“智能推荐”正在悄悄毁掉你的文档专业性(含字体嵌入漏洞、段前间距陷阱、目录生成断链详解)
  • MVVM Helpers分组功能全解析:Grouping类实现优雅数据展示
  • Ubuntu 26.04 LTS组件更替:Totem与GNOME系统监视器的现代化替代方案
  • Qt模型/视图架构:从MVC解耦到自定义Model实现
  • 5大核心问题:如何高效准备数据科学面试?
  • 如何用Blackbird开源工具实现3倍效率的数字足迹调查
  • JAVA练习316- 路径总和 III
  • 【回眸】搞钱灵感——二手经济项目利弊分析与落地策略
  • NE6212 超快关断同步整流控制器
  • 如何用Infinite-Canvas实现ComfyUI本地部署与远程工作流管理?终极指南
  • 游戏UI自动化测试终极方案:Poco框架实战指南
  • Label Studio终极指南:如何在5分钟内搭建你的多模态AI数据标注平台
  • 吃透一阶HDI分层底层架构,打通高密度布线入门思路
  • 课程资料管理系统
  • 鸿蒙新特性 | 图片怎么显示——Image 组件那些坑
  • David未来路线图:依赖管理工具的发展趋势与展望
  • PARD2-Qwen3-14B社区资源大全:开源项目、工具链与最佳实践分享
  • AM263P微控制器复位与时钟系统配置实战指南
  • 【2024最简AI分析路径】:无需编程基础,用低代码+AutoML 48小时内产出CEO级决策看板
  • ​​【前端开发】Vue CLI 命令及速查表
  • Socket.IO Redis Emitter在生产环境中的部署策略:Docker与Kubernetes实践
  • Loop for Mac:优雅高效的macOS窗口管理革命
  • Triangles性能优化指南:如何在高分辨率下保持流畅渲染