在分布式系统中,往往会遇到需要保证跨服务、跨系统的数据一致性场景。通常可以采用分布式事务的方式来保证数据一致,但传统的两阶段提交(XA 等)在性能和复杂度上都较为昂贵。RocketMQ 通过事务消息 (Transaction Message) 提供了一种最终一致性解决方案,将本地事务与消息发送进行半同步耦合,可在业务上实现类似“分布式事务”的效果。
本文将为你介绍 RocketMQ 事务消息的原理、使用流程、关键配置与常见注意事项,并结合示例说明如何在实际项目中应用。
1. 事务消息的核心原理
RocketMQ 事务消息与普通消息不同之处在于,它引入了“三段式”提交流程:
-
发送半消息 (Half Message)
- Producer 首先向 Broker 发送一个“半消息(Half Message)”,Broker 会先把这条消息持久化,但并不对消费者可见。
- 同时返回给 Producer 一个发送结果(其中包含
msgId
等信息)。此时对 Consumer 而言,这条消息并不存在于其可消费范围内。
-
执行本地事务
- Producer 在收到 Half Message 成功发送的响应后,本地开始执行自己的业务逻辑/事务操作(如数据库更新、扣减库存等)。
- 执行完成后,需要根据结果告诉 Broker:是要提交还是回滚该消息。
-
提交或回滚消息
- 如果本地事务成功,Producer 会向 Broker 发送一个“commit”请求,Broker 会把该半消息标记为可见,变为真正的消息,Consumer 就能消费到它。
- 如果本地事务失败,则发送“rollback”请求,Broker 会将半消息删除或忽略,从而不会被消费者看到。
为了防止 Producer 因故障(如宕机)而没有及时发送“commit”或“rollback”,Broker 引入了事务回查 (Transaction Check) 机制:
- Broker 会定期向 Producer 询问(回查)未完成状态的半消息,Producer 必须实现相应的回查接口,从而再次判定本地事务是成功还是失败。
- 如果 Producer 仍然无法给出明确结果,Broker 会继续进行回查,直到达到一定重试上限后再采取默认处理(通常为回滚消息)。
时序图
Producer Broker
| |
|---(1) Send half message-------> | (消息先持久化, 状态=PREPARED)
|<----------- Result ------------ |
| |
|---(2) Do local transaction ---> | (Producer 本地执行业务操作)
| (DB / RPC ... ) |
| |
|---(3) Commit/Rollback---------> | (更新消息状态=COMMITED / ROLLBACK)
| |
(若超时或 Producer 异常导致状态不明确, Broker 发起回查)
|<------- Check message---------- |
|---(回查逻辑: 再次确认本地事务)-- |
2. 典型使用场景
- 订单与支付:在用户支付完成后,需要保证订单状态与支付流水都一致更新,否则容易出现“支付成功但订单未更新”或“订单已完成但支付失败”等不一致状况。
- 库存扣减与订单创建:先发送“预扣库存消息”,在本地操作中扣减库存成功后,再提交消息通知后续流程;若扣减失败,则回滚。
- 积分发放:在用户下单成功后,需发放积分。在保证下单成功后再提交消息,保证不会出现“订单没成功却发放了积分”的情况。
总的来说,适合需要在不同系统之间协调操作结果,而且必须保证操作与消息发送的原子性的场景。
3. 事务消息的使用流程
3.1 Producer 侧
(1)创建 TransactionMQProducer
使用 Java 客户端时,我们需要实例化一个 TransactionMQProducer
,并实现回调方法,用于处理本地事务状态的回查:
// 1. 创建 TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
producer.setNamesrvAddr("127.0.0.1:9876");
// 2. 实现 TransactionListener
TransactionListener transactionListener = new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 在这里执行本地事务,例如数据库操作
// arg 可传入业务参数
try {
// TODO: 执行业务操作, 如果成功:
return LocalTransactionState.COMMIT_MESSAGE;
}