连载中 10/20

事务消息:本地事务与发消息的原子性

2026-08-03 · 2181 阅读 · 0 评论 · 0 赞

经典两难:先写库还是先发消息

第 2 篇立过一块碑:订单落库和消息发送是两个系统的事,数据库事务管不到 MQ。把这个两难摆开:

  • 先写库,再发消息:两步之间进程崩溃,库里有订单,消息永远没发出去——下游全不知情;
  • 先发消息,再写库:消息发出去了,落库失败——下游拿着一条幽灵订单开始发积分。

第 5 篇的本地消息表是工程界的第一答案:把消息记录塞进业务库,用同一个事务捆住。RocketMQ 则把这个思路内置到了 Broker 里,叫事务消息(半消息机制)

半消息机制的四步舞

完整流程是这样:

1 生产者  -> 发送半消息(half message) -> Broker 存储
          <- 返回成功,但此时消息对消费者不可见
2 生产者  -> 执行本地事务(订单落库)
3 生产者  -> 根据事务结果,提交(消息可见)或回滚(消息删除)
4 Broker  -> 若迟迟等不到二次确认,主动回查生产者:那笔事务到底成没成?

关键设计在第一步和第四步。半消息先存进系统内部 Topic(RMQ_SYS_TRANS_HALF_TOPIC),消费者看不见——这就同时规避了「发早了」和「发丢了」:事务成功前它不存在于业务视野,事务成功后它必然已安全落盘。

代码长什么样

TransactionMQProducer producer = new TransactionMQProducer("order_group");
producer.setTransactionListener(new TransactionListener() {

    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            orderService.create((Order) arg);          // 本地事务
            return LocalTransactionState.COMMIT_MESSAGE; // 成功,放行
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查:查数据库的真实状态,绝不查内存变量
        boolean exists = orderMapper.exists(msg.getKeys());
        return exists ? LocalTransactionState.COMMIT_MESSAGE
                      : LocalTransactionState.UNKNOW;   // 拿不准就再等等,别乱回滚
    }
});
producer.sendMessageInTransaction(msg, order);

回查的两个要点

第一,回查逻辑必须查库。回查发生时,发消息的那个进程可能已经重启了八回,内存里的标志位早没了。唯一可信的依据是数据库里事务的最终状态——这也要求业务表设计上能表达「这笔业务成没成」。

第二,拿不准就返回 UNKNOW。查库超时、主从延迟、状态还是中间态,都返回 UNKNOW 让 Broker 稍后再问。回查有次数上限(默认 15 次),超限后 Broker 丢弃半消息并记日志——配好告警,别让消息悄无声息地死掉。

事务消息和本地消息表怎么选

对比项事务消息本地消息表
存储位置Broker 内部 Topic业务库自己的表
补偿机制Broker 主动回查自己写定时任务扫描
代码侵入改用事务生产者+两个回调业务代码里多写一张表+任务
运维成本零额外组件,但绑定 RocketMQ任何 MQ 都能用,通用性强
可见性消息状态在 MQ 控制台可查SQL 直接查,排查直观

选型不复杂:RocketMQ 用户直接享受事务消息的现成回查;多 MQ 混用或想保留排查直观性的,本地消息表依然是利器。两者的可靠性是同一档的,区别只是「谁替你做补偿」

它保证什么,不保证什么

事务消息保证的是:本地事务成功,消息一定送达 Broker 且最终对消费者可见——本地事务与「发消息」这个动作的原子性。它不保证下游执行成功:积分服务收到消息还是可能失败、可能重复。所以下游的老三样一个不能少:消费重试、幂等、死信告警。事务消息只是把最难的「第一跳」焊死了,整条链路的可靠性仍然是各跳各自负责。

小结

半消息机制的精髓:先把消息「藏」起来保证不存在假消息,事务成功后再「放」出来保证不会丢消息,Broker 回查兜住一切意外。回查必须查库,拿不准就 UNKNOW,两条写进代码评审清单。

生产端的课题到这里全部完结。下一站轮到消费端:消费失败与重试——重试队列怎么工作,死信队列里的消息谁来管,以及一条毒消息的自救流程

503

10 年全栈工程师 · 503咖啡馆主理人

#消息队列#事务消息#半消息#消息回查#RocketMQ

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞