事务消息中的“回查机制”是如何工作的?如果回查一直失败,RocketMQ 会如何处理?
在 RocketMQ 的事务消息中,回查机制(Transaction Check Mechanism)是保证分布式事务最终一致性的核心保障兜底方案。它主要用于解决两阶段提交(2PC)中,因为网络闪断、生产者重启等原因导致 Broker 迟迟未收到二次确认(Commit 或 Rollback)的问题。
下面详细拆解回查机制的工作原理以及 RocketMQ 对持续失败的处理方式。
一、 回查机制是如何工作的?
事务消息的整体流程是:发送半消息(Half Message) -> 执行本地事务 -> 发送二次确认(Commit/Rollback)。
当第三步发生异常(丢失或超时),回查机制就会介入。具体流程如下:
1. 触发回查的条件
当 Broker 接收到半消息(Half Message)后,会将消息存储在一个特殊的内部 Topic(RMQ_SYS_TRANS_HALF_TOPIC)中。如果经过了一段时间(默认是 1 分钟),Broker 仍然没有收到该消息的 Commit 或 Rollback 指令,就会主动向生产者发起回查请求。
2. 回查的执行流程
- Broker 寻找客户端:Broker 会根据半消息所属的
ProducerGroup,在当前连接的生产者集群中随机挑选一台存活的 Producer 节点发起回查请求。(注意:不一定是最初发送消息的那台机器,因此本地事务状态必须是全局可查的,通常存在数据库中)。 - 执行回查逻辑:Producer 收到回查请求后,会触发开发者实现并注册的
TransactionListener接口中的checkLocalTransaction()方法。 - 返回回查结果:开发者在
checkLocalTransaction()方法中,通过检查数据库中本地事务的执行结果(例如查询业务流水表或事务状态表),向 Broker 返回以下三种状态之一:COMMIT_MESSAGE:本地事务已成功。Broker 会将半消息转换为普通消息投递给消费者。ROLLBACK_MESSAGE:本地事务已失败。Broker 会直接丢弃(删除)该半消息,消费者永远不会收到。UNKNOWN:本地事务状态未知(可能还在执行中,或者数据库查询超时)。Broker 会保持半消息状态,等待下一次回查。
二、 如果回查一直失败,RocketMQ 会如何处理?
如果回查一直返回 UNKNOWN,或者因为网络/客户端原因回查请求一直超时失败,RocketMQ 不会无休止地进行回查,以防止内部积压大量无法决断的半消息耗尽资源。
RocketMQ 的具体处理策略如下:
1. 最大回查次数限制
RocketMQ 为事务回查设置了最大次数限制,由 Broker 端的配置参数 transactionCheckMax 控制,默认值是 15 次。
2. 回查间隔
默认情况下,RocketMQ 的回查间隔是 1 分钟(早期版本受限于定时消息的精度,现在版本内部有单独的定时任务控制)。
3. 达到最大次数后的处理:默认丢弃(Rollback)
如果一个半消息的回查次数达到了 transactionCheckMax(默认 15 次),并且最后一次回查结果依然是 UNKNOWN 或失败:
- RocketMQ 默认会将该半消息当作 ROLLBACK 处理。
- 也就是说,Broker 会直接丢弃该消息,消费者永远不会消费到这条消息。
- 原因:如果一个本地事务经过了 15 分钟(15次)还无法确定结果,大概率是本地事务执行失败或发生了严重的系统异常,为了保护 MQ 系统自身的稳定性,选择丢弃是相对安全的兜底策略。
4. 可自定义丢弃策略(扩展点)
如果你不希望达到 15 次后直接丢弃,可以通过重写 Broker 的 AbstractTransactionalMessageCheckListener 类或修改 Broker 源码来改变这一默认行为,比如将其放入死信队列(DLQ)并触发人工告警。但在绝大多数业务场景下,默认的丢弃策略配合业务监控已经足够。
三、 生产环境的最佳实践(防坑指南)
为了配合好 RocketMQ 的回查机制,开发者在编写代码时必须注意以下几点:
引入“本地事务状态表”:
- 痛点:Broker 可能向 Producer 集群的任意一台机器发起回查,如果只靠内存状态是无法回查的。
- 方案:在执行本地业务(如扣减库存)时,在同一个数据库事务中,向一张专门的
local_transaction_log表写入一条记录(包含全局唯一的事务ID/消息ID)。 - 回查逻辑:
checkLocalTransaction()方法只需要去数据库SELECT这张表,如果有记录,返回COMMIT;如果没有记录,返回UNKNOWN或ROLLBACK。
正确区分
ROLLBACK和UNKNOWN:- 在回查时,如果数据库查不到记录,不要立刻返回
ROLLBACK。因为可能是本地数据库事务还没 Commit(还在阻塞),而回查请求先到了。 - 应该检查消息的生成时间,如果距离现在很短(比如几秒),应返回
UNKNOWN让其稍后重试;如果距离现在已经很久(比如 10 分钟)依然查不到流水,才可以安全地返回ROLLBACK。
- 在回查时,如果数据库查不到记录,不要立刻返回
保证回查方法的幂等性和快速返回:
checkLocalTransaction方法不应该包含复杂的业务逻辑或耗时的远程调用,只应单纯地查询本地事务状态,否则会导致 Broker 回查线程池阻塞。