连载中 10/20

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

2026-08-03 · 2185 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞