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