bigywater.tech
分布式事务 · 13

事务消息:半消息与回查

事务消息(半消息 + 回查)想让“本地事务提交”和“消息发出”要么都成要么都不成。它怎么保证?本地事务结果丢了怎么办?生产者崩溃后谁来回答?回查答不上来又会怎样?这篇用真实的 RocketMQ 和 MySQL,把这六种情形逐个跑出来,也撞上了一个新生产者收不到回查的坑。

1643 ms
半消息发出之后、本地事务提交之前,消费者看不到消息的这段时间(提交后 13 ms 收到)
0 次 vs 1 次
生产者 kill -9 后,没声明 topic 与声明了 topic 的新生产者收到回查的次数
3 次
回查一直 UNKNOW 时(本篇配置的上限),broker 回查几次后丢弃半消息

半消息

实测 · 本机执行输出
          0 ms  [生产者] HALF_SENT o-1
          0 ms  [生产者] LOCAL_TX_BEGIN
       1643 ms  [生产者] LOCAL_TX_COMMITTED
       1645 ms  [生产者] SEND_RESULT SEND_OK local=COMMIT_MESSAGE
       1656 ms  [消费者] CONSUMED o-1
  半消息发出后 1643 ms 本地事务才提交,这段时间消费者什么都没收到;提交后 13 ms 消费者收到 o-1;订单库里 o-1 有 1 行

先把半消息发到 broker,此时消费者看不到;本地事务提交后返回 COMMIT_MESSAGE,消息才对消费者可见。

回滚

实测 · 本机执行输出
          0 ms  [生产者] HALF_SENT o-2
          0 ms  [生产者] LOCAL_TX_BEGIN
        620 ms  [生产者] LOCAL_TX_ROLLED_BACK
        622 ms  [生产者] SEND_RESULT SEND_OK local=ROLLBACK_MESSAGE
  等了 5 秒:消费者收到 o-2 的次数 0;订单库里 o-2 有 0 行

本地事务回滚返回 ROLLBACK_MESSAGE,半消息被丢弃,下游不会看到没成功的订单。

回查

实测 · 本机执行输出
          0 ms  [生产者] HALF_SENT o-3
        146 ms  [生产者] LOCAL_TX_COMMITTED (listener returns UNKNOW)
        149 ms  [生产者] SEND_RESULT SEND_OK local=UNKNOW
       5502 ms  [生产者] CHECK_BACK o-3 times=1 订单存在=true -> COMMIT
       5513 ms  [消费者] CONSUMED o-3
  回查发生在半消息发出 5502 ms 之后(transactionTimeOut = 3000 ms);回查 1 次;消费者收到 o-3 1 次

本地事务结果没告诉 broker(UNKNOW),broker 等 transactionTimeOut 之后回查,生产者查库:订单在就 COMMIT。

生产者崩溃后由谁回答

实测 · 本机执行输出
          0 ms  [旧生产者] kill -9(订单已提交,事务结果没有告诉 broker)
        385 ms  [新生产者(没声明 topic)] PRODUCER_STARTED
  订单库里 o-4 有 1 行;消费者收到 o-4 0 次;新生产者收到回查 0 次。broker 的 transaction.log 里和这条半消息有关的行(4 行):
    22:16:28 WARN Transaction-msg-check-thread2 - Check transaction failed, channel is null. groupId=pg-order
    22:16:31 WARN Transaction-msg-check-thread1 - Check transaction failed, channel is null. groupId=pg-order
    22:16:34 WARN Transaction-msg-check-thread2 - Check transaction failed, channel is null. groupId=pg-order
    22:16:37 INFO TransactionalMessageCheckService - Put checked-too-many-time half message to TRANS_CHECK_MAXTIME_TOPIC OK. Restored in queueOffset=7, co
          0 ms  [旧生产者] kill -9(o-4b,订单已提交)
        461 ms  [新生产者(声明了 topic)] PRODUCER_STARTED
       2997 ms  [新生产者(声明了 topic)] CHECK_BACK o-4b times=1 订单存在=true -> COMMIT
       3003 ms  [消费者] CONSUMED o-4b
  订单库里 o-4b 有 1 行;消费者收到 o-4b 1 次 —— 声明了 topic 的新生产者启动时就取了路由、向 broker 注册,broker 才找得到“能回查的人”;事务结果由能查本地数据库的人在回查里补上

kill -9 后换一个新生产者(同一个 group)上线。只启动、没声明 topic 的新生产者没有向 broker 注册,broker 回查找不到人,3 次后丢弃半消息,订单已提交而消息没发出;声明了 topic 的新生产者启动时取路由并注册,回查到达,按订单是否存在回答。

本地事务没提交

实测 · 本机执行输出
       4368 ms  [新生产者] CHECK_BACK o-5 times=1 订单存在=false -> ROLLBACK
  等了 5 秒:订单库里 o-5 有 0 行;消费者收到 o-5 0 次

崩溃发生在提交之前,回查发现订单不在,回复 ROLLBACK,消息不发出,两边一致。

回查答不上来

实测 · 本机执行输出
          0 ms  [生产者] HALF_SENT o-6
         97 ms  [生产者] LOCAL_TX_COMMITTED (listener returns UNKNOW)
         99 ms  [生产者] SEND_RESULT SEND_OK local=UNKNOW
       3423 ms  [生产者] CHECK_BACK o-6 times=1 -> UNKNOW(查不出来)
       6426 ms  [生产者] CHECK_BACK o-6 times=2 -> UNKNOW(查不出来)
       9430 ms  [生产者] CHECK_BACK o-6 times=3 -> UNKNOW(查不出来)
  broker 的 transaction.log:22:17:07 INFO TransactionalMessageCheckService - Put checked-too-many-time half message to TRANS_CHECK_MAXTIME_TOPIC OK. Restored in queueOffset=15, c
  回查共 3 次;消费者收到 o-6 0 次;订单库里 o-6 有 1 行(本地事务其实提交了,但消息被 broker 丢掉了:本地有订单、下游收不到)

一直返回 UNKNOW,回查到上限后 broker 丢弃半消息:本地有订单、下游收不到。回查必须能给出确定答案。

分步演示

事务消息
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用官方 bin 包的 RocketMQ 5.5.1(NameServer + 单 Broker,Java 17,客户端 rocketmq-client 5.5.1 的 TransactionMQProducer / DefaultMQPushConsumer),本地事务写 MySQL 26.7.0(Connector/J 26.7.0);broker 配置 transactionTimeOut=3000 ms、transactionCheckInterval=3000 ms、transactionCheckMax=3(默认 15);JVM 的 user.home 指到实验目录,日志不写进家目录;半消息可见性、回查结果由生产者与消费者进程的输出时间戳和 broker 的 transaction.log 佐证
    • 没有验证:RocketMQ 的 gRPC 客户端(rocketmq-clients)与 Proxy 模式下的事务消息;多个 broker / 多个生产者实例时回查具体发给哪一个;默认配置(transactionCheckMax=15、回查间隔 30 秒)下的表现;“没声明 topic 的新生产者收不到回查”的原因我只用 broker 日志(channel is null)与“声明了 topic 就收到”的对照佐证,没有读 RocketMQ 的客户端源码确认其注册流程;消费端重复投递(第 14、15 篇)
    京ICP备2026067502号-1京公网安备11010502063487号