【RocketMQ】事务消息详解

【RocketMQ】事务消息详解

【一】什么是 RocketMQ 事务消息

RocketMQ 事务消息用于解决分布式事务问题,实现本地数据库事务与消息发送的原子性:

要么本地事务执行成功 + 消息投递成功;要么本地事务失败 + 消息完全不投递;避免出现:数据库成功消息没发 / 消息发出去数据库失败这种数据不一致。

总结:RocketMQ 事务消息保证生产端的事务和消息投递状态同步,要么都成功,要么都失败。

注意:RocketMQ 事务消息不能保证消费端事务原子性,只保证「发送消息 + 本地事务」的原子性;消费端还需要自己做幂等。

【二】使用场景

核心场景:跨系统 / 跨服务,本地 DB 操作要和发消息绑定原子性

【1】订单创建后扣减库存

订单服务创建订单(本地事务),同时发送消息给库存服务扣库存。

如果订单入库成功,消息没发出去 → 库存不减,订单处于已创建无扣减,数据不一致;

如果消息先发,订单入库失败 → 库存扣了,订单没生成。

用事务消息:订单创建成功消息对外可见;订单回滚,消息直接丢弃。

【2】支付完成,发送通知、积分、优惠券

支付服务本地更新支付记录,同时发消息:给用户加积分、发优惠券、推送短信。

支付成功才允许消息被消费;支付失败消息不能被消费。

【3】业务流程异步解耦,需要强一致性

业务 A 完成本地数据库操作后,触发下游多个异步业务,不能出现下游收到消息但上游数据库回滚。

【4】分布式链路中,本地事务触发下游异步调用

比如:用户注册成功,初始化账户、生成账单,多个下游服务异步处理。

❌ 不适合场景

(1)需要多服务数据库同时原子提交(2PC 那种强一致性),事务消息最终一致性,不是强一致;

(2)短同步链路,不需要异步;

(3)追求极低延迟:事务消息有半消息、回查逻辑,会有额外耗时。

【三】底层原理

RocketMQ 事务消息分为 半消息 (Prepare 消息) → 执行本地事务 → 提交 / 回滚消息 → 事务回查

【1】完整流程

(1)发送半消息(Prepare 消息)

Producer 发送事务半消息给 Broker;Broker 收到消息标记为 TRANSACTION_PREPARED,这条消息对消费者不可见,消费者消费不到。返回成功给生产者。

半消息:消息已经存 broker,但是不会投递消费队列。

(2)Producer 执行本地事务

收到半消息发送成功响应后,Producer 执行自己本地数据库事务(执行业务逻辑)。本地事务执行结果三种状态:

(1)COMMIT_MESSAGE:本地事务成功 → 通知 Broker 提交消息,消息正式可见,消费者可以消费

(2)ROLLBACK_MESSAGE:本地事务失败 → 通知 Broker 回滚,消息直接删除,永远不会被消费

(3)UNKNOWN:状态未知(网络抖动、生产者宕机,没返回 commit/rollback),Broker 需要事务回查

(3)Broker 事务回查(核心容错机制)

当 Broker 收到半消息,长时间没有收到 commit/rollback 指令;Broker 会定时主动回调 Producer 的checkLocalTransaction接口,查询本地事务当前真实状态。

(1)如果本地事务已经 commit → Broker 提交消息

(2)如果本地事务已经 rollback → Broker 删除消息

(3)如果还是 UNKNOWN,等待下一次回查,回查有最大次数,超过直接回滚丢弃。

⚠️关键点:回查依赖业务实现检查本地事务状态接口;如果生产者宕机重启,重启后 Broker 依旧会来回查。

【2】存储层面

(1)半消息实际存储在 RMQ_SYS_TRANS_HALF_TOPIC 这个系统 topic,不是业务 topic;

(2)commit 之后,消息复制到真正业务 topic,对外可见;

(3)rollback 直接删除半消息;

(4)事务回查 offset 存储在 RMQ_SYS_TRANS_OP_HALF_TOPIC。

【3】时序图文字版

bash 复制代码
Producer                     Broker
   |  sendHalfMessage ------>| 保存半消息,TRANSACTION_PREPARED,不可消费
   |<-------send ok -------- |
   | 执行本地DB事务
   |
   |----commit/rollback----->| 更新消息状态
   |                         | commit:投递真实topic;rollback删除
   
# 如果生产者宕机,没返回commit/rollback
Broker定时轮询半消息
Broker --回调checkLocalTransaction--> Producer
Producer查询数据库本地事务状态返回
Broker根据返回结果提交/回滚

【四】优缺点

✅优点

(1)实现本地事务与消息发送原子性,保证上游业务 DB 成功消息才对外可见;

(2)异步解耦,下游业务异步执行,提升吞吐量;

(3)有事务回查机制,生产者宕机重启后依然可以完成事务状态确认,容错能力强;

(4)基于 RocketMQ 原生支持,不需要额外引入中间件。

❌缺点

(1)只保证发送端原子性,消费端不保证。消费失败需要消费端重试 + 幂等;

(2)需要业务实现本地事务状态检查接口,业务侵入;业务必须提供查询本地事务执行状态的能力;

(3)事务回查会带来一定延迟,不适合极致低延迟场景;

(4)如果业务没有做好幂等,多次回查 + 消息重试会导致重复消费;

(5)不适合跨多个服务本地事务(仅保证生产者本地 DB + 发消息原子;不能同时保证 A、B 两个服务 DB 同时原子);

(6)半消息会占用 broker 存储;大量事务消息会增加 Broker 压力;

(7)回查频率、最大回查次数需要合理调参,设置不当会出现消息悬停。

常见坑:业务没有实现 checkLocalTransaction,或者查询逻辑写错,导致消息一直回查,最后丢弃,业务消息丢失。

【五】替换方案(分布式事务方案对比)

最常用替代:本地消息表方案,很多生产环境不使用 RocketMQ 事务消息,而是自己实现本地消息表,避免 RocketMQ 事务消息的回查、半消息的各种坑。

替代方案示例:本地消息表

很多项目不使用 RocketMQ 事务消息,选择本地消息表。原理:同一个本地事务,同时写入业务数据 + message_record 消息表;然后定时任务扫描消息表发送 MQ,更新消息状态。

bash 复制代码
id	biz_id	topic	content	status	retry_count	create_time
1	order_1001	ORDER_CREATE_TOPIC	xxx	0 待发送	0	2026‑08‑19

status:0 待发送;1 发送成功;2 发送失败

java 复制代码
@Transactional
public void createOrderWithMsg(Long orderId){
    //1.本地事务内,插入订单
    orderMapper.insert(order);
    //2.同一个事务,插入消息记录,状态=待发送
    MessageRecord record = new MessageRecord();
    record.setBizId(orderId.toString());
    record.setTopic("ORDER_CREATE_TOPIC");
    record.setContent("xxx");
    record.setStatus(0);
    messageRecordMapper.insert(record);
}

定时任务:扫描 status=0,重试次数少的消息,发送 MQ;发送成功更新 status=1;失败重试 + retry_count++。

如果发送失败,定时任务不断重试,直到发送成功。

优点:不依赖 MQ 事务能力,任意 MQ 都能用;逻辑可控;缺点:需要维护一张消息表,定时任务。

【六】代码详细案例(SpringBoot + RocketMQ 事务消息)

【1】maven依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

【2】application.yml

yml 复制代码
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: ORDER_TRANS_PRODUCER_GROUP

业务场景:创建订单(本地数据库事务),事务消息发送,库存服务消费消息扣库存。

数据库表:t_order(订单表),order_id 作为事务唯一 id。

【3】实现 RocketMQLocalTransactionListener

两个核心方法:

executeLocalTransaction:半消息发送成功后,执行本地事务

checkLocalTransaction:Broker 回查,检查本地事务状态

java 复制代码
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

@RocketMQTransactionListener(txProducerGroup = "ORDER_TRANS_PRODUCER_GROUP")
@Component
public class OrderTransactionListener implements RocketMQLocalTransactionListener {

    // 注入自己的service,操作数据库
    private final OrderService orderService;

    public OrderTransactionListener(OrderService orderService) {
        this.orderService = orderService;
    }

    /**
     * 半消息发送成功之后,执行本地事务
     * @param msg mq消息
     * @param arg 业务自定义参数,这里传入orderId
     * @return
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Long orderId = (Long) arg;
        try {
            // 执行本地数据库事务:创建订单
            orderService.createOrder(orderId);
            // 本地事务成功,通知Broker提交消息,消息对消费者可见
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            // 本地事务失败,回滚,消息删除
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * Broker事务回查:生产者宕机,没有返回commit/rollback,broker回调这个接口查询本地事务状态
     * 重点:根据消息拿到业务唯一id,查询数据库,判断本地事务到底成功还是失败
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // 从消息header取出orderId
        String orderIdStr = (String) msg.getHeaders().get("ORDER_ID");
        Long orderId = Long.parseLong(orderIdStr);

        // 查询数据库,看订单是否创建成功
        boolean exist = orderService.isOrderExist(orderId);
        if(exist){
            // DB已经成功,提交消息
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            // DB没有数据,事务回滚
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}

【4】生产者发送事务消息

java 复制代码
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;

@Service
public class OrderMqService {

    private final RocketMQTemplate rocketMQTemplate;
    public static final String ORDER_TOPIC = "ORDER_CREATE_TOPIC";

    public OrderMqService(RocketMQTemplate rocketMQTemplate) {
        this.rocketMQTemplate = rocketMQTemplate;
    }

    public void sendOrderCreateTxMsg(Long orderId){
        Message<String> message = MessageBuilder
                .withPayload("订单创建消息,orderId="+orderId)
                .setHeader("ORDER_ID",orderId.toString())
                .build();
        //发送事务消息,第二个参数是业务参数,会传给executeLocalTransaction的arg
        rocketMQTemplate.sendMessageInTransaction(
                ORDER_TOPIC,
                message,
                orderId
        );
    }
}

【5】OrderService 业务层

java 复制代码
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long orderId){
        //本地事务:插入订单
        Order order = new Order();
        order.setOrderId(orderId);
        order.setStatus(1);
        orderMapper.insert(order);
        // 模拟异常,测试回滚
        // int i = 1/0;
    }

    //给事务回查接口调用,查询事务状态
    public boolean isOrderExist(Long orderId){
        return orderMapper.selectById(orderId) != null;
    }
}

【6】消费者(库存服务,注意消费端必须做幂等)

⚠️重点:RocketMQ 事务消息只保证发送端原子;消费端会重试,必须做幂等,用 orderId 判断是否已经扣过库存。

java 复制代码
@RocketMQMessageListener(topic = "ORDER_CREATE_TOPIC", consumerGroup = "STOCK_CONSUMER_GROUP")
@Component
public class StockConsumer implements RocketMQListener<String> {

    private final StockService stockService;
    public StockConsumer(StockService stockService) {
        this.stockService = stockService;
    }

    @Override
    public void onMessage(String message) {
        //解析orderId
        Long orderId = parseOrderId(message);
        //幂等判断:如果该订单已经扣过库存,直接return,不再扣减
        if(stockService.hasDeduct(orderId)){
            return;
        }
        //执行扣库存
        stockService.deductStock(orderId);
    }

    private Long parseOrderId(String message){
        //业务自行解析
        return 1L;
    }
}

【七】注意事项

(1)checkLocalTransaction回查接口必须保证幂等、查询数据库不能查缓存,要查真实 DB 状态;

(2)消费端一定要做幂等,事务消息解决发送端原子,消费失败会重试;

(3)合理设置 Broker 事务回查参数:transactionTimeOut、checkMaxTimes;

(4)不要把业务耗时很长的逻辑放在executeLocalTransaction,半消息超时会触发大量回查;

(5)事务 producer group 必须唯一,一个 group 对应一组事务监听;

(6)如果回查多次还是 UNKNOWN,broker 会直接回滚丢弃消息,业务要监控事务消息丢弃告警。

常见问题

Q:RocketMQ 事务消息能保证消费一定成功吗?

A:不能。只能保证消息一定投递到 broker,上游 DB 成功消息才可见;消费失败会重试,消费成功需要业务自己保证。

Q:半消息会占用磁盘吗?

A:会,存储在系统半消息 topic,commit/rollback 之后才清理。

Q:和本地消息表怎么选?

A:简单业务可以用 RocketMQ 事务消息;复杂生产系统,追求可控性,优先本地消息表。

相关推荐
肠畔码农4 小时前
深度解析 RocketMQ 死信队列(DLQ):消费重试的终点站、存储隔离与架构容灾本质
架构·rocketmq
阿昌喜欢吃黄桃1 天前
RocketMQ 同一 JVM 启动多个 Producer 导致 NameServer 只连一个的问题
jvm·rocketmq·mq
lazy H2 天前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
二炮手亮子4 天前
消息队列篇——RocketMQ
rocketmq
笨鸟飞不快4 天前
RocketMQ Lite Topic 流程梳理与设计解读
后端·rocketmq
Rain的Java大神之路6 天前
MQ重复消费问题怎么解决
java·经验分享·后端·面试·架构·rabbitmq·rocketmq
hopsky7 天前
《RocketMQ 技术内幕:RocketMQ 架构设计与实现原理》核心内容详解
rocketmq
小园子的小菜10 天前
RocketMQ 核心原理、架构优势与核心机制全解
架构·rocketmq
晚安code17 天前
RocketMQ 顺序消费实战:批量接口加 Redis Pipeline同步数据
redis·rocketmq