RocketMQ 事务消息解决的是“本地事务执行结果与消息投递如何协调”的问题。关键不只是调用发送方法,而是让本地事务结果能够被可靠查询;回查时不能凭内存标记或一律返回成功。

从半消息到二次确认

生产者先发送暂不可消费的半消息,Broker 确认后再执行本地事务,并根据结果提交或回滚消息。提交后才能被消费者取得。若网络中断、进程退出或结果暂不确定,Broker 会向生产者回查。RocketMQ 事务消息机制

这协调的是消息生产与本地事务,不是把消费者数据库也放进同一强一致事务。消费失败重试与消费者业务幂等仍需单独设计。

发送时携带可查询的业务标识

下面按 RocketMQ-Spring 2.2.3 的 API 说明调用关系。片段位于已经注入 RocketMQTemplate 的组件中,topic 与 tag 合写为 destination,不是两个独立参数:

String transactionId = java.util.UUID.randomUUID().toString();
org.springframework.messaging.Message<String> message =
        org.springframework.messaging.support.MessageBuilder
                .withPayload(transactionId)
                .setHeader("transactionId", transactionId)
                .build();
rocketMQTemplate.sendMessageInTransaction(
        "order-events:created", message, null);

示例只用事务标识作消息体,实际业务应定义事件结构。arg 可以携带执行上下文,但回查不能依赖它,必须能从消息取得标识并查询持久存储。2.2.3 发送 API

两个回调分别做什么

该版本监听器只有本地执行和状态回查两个回调,没有 destroy 方法。下面是适配层结构;LocalTransactions 是业务接口,需要另行提供数据库实现并注册为 Spring Bean:

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;

@RocketMQTransactionListener
public class OrderTransactionListener
        implements RocketMQLocalTransactionListener {
    public interface LocalTransactions {
        RocketMQLocalTransactionState executeOnce(String transactionId);
        RocketMQLocalTransactionState findState(String transactionId);
    }

    private final LocalTransactions transactions;

    public OrderTransactionListener(LocalTransactions transactions) {
        this.transactions = transactions;
    }

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(
            Message message, Object arg) {
        try {
            return transactions.executeOnce(id(message));
        } catch (RuntimeException exception) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        try {
            return transactions.findState(id(message));
        } catch (RuntimeException exception) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

    private String id(Message message) {
        Object value = message.getHeaders().get("transactionId");
        if (!(value instanceof String) || ((String) value).isEmpty()) {
            throw new IllegalArgumentException("缺少 transactionId");
        }
        return (String) value;
    }
}

实现必须让业务变更与最终状态在同一数据库事务提交,以事务标识唯一约束保证重复执行安全,并确认事务调用经过正确代理。已提交返回 COMMIT,明确失败且不会再成功返回 ROLLBACK;缺记录、处理中、查询异常或尚不能确认结果返回 UNKNOWN。缺记录不一定等于回滚,还可能处于执行与回查交错的窗口。2.2.3 回调接口

不要把提交状态当作默认值

不能先提交业务、再独立记录成功:两步之间宕机会丢失判断依据。异常和长期 UNKNOWN 需要监控告警,不能用 COMMIT 掩盖;回查次数及时间也受 Broker 配置约束。

消费者仍需按事件或业务标识去重,避免重试重复扣款、发券。这段示例展示回调契约,不包含数据库事务、补偿任务或消费者实现,不能直接替代完整业务方案。