RocketMQ事务消息
RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。
Half Message(半消息)
是指暂不能被Consumer消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递
状态,处于该种状态下的消息称为半消息。需要 Producer
对消息的二次确认
后,Consumer才能去消费它。
消息回查
由于网络闪段,生产者应用重启等原因。导致 Producer 端一直没有对 Half Message(半消息) 进行 二次确认。这是Brock服务器会定时扫描长期处于半消息的消息
,会
主动询问 Producer端 该消息的最终状态(Commit或者Rollback),该消息即为 消息回查。
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
事务消息发送及提交:
- 发送消息(half消息)
- 服务端响应消息写入结果
- 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
- 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
补偿流程:
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
- Producer收到回查消息,检查回查消息对应的本地事务的状态
- 根据本地事务状态,重新Commit或者Rollback
补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
一阶段的消息如何对用户不可见
事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。
如何做到写入了消息但是对用户不可见?——写入消息数据,但是不创建对应的消息的索引信息。
熟悉RocketMQ的同学应该都清楚,消息在服务端的存储结构如上,每条消息都会有对应的索引信息,Consumer通过索引读取消息。
那么实现一阶段写入的消息不被用户消费(需要在Commit后才能消费),只需要写入Storage Queue,但是不构建Index Queue即可。
RocketMQ中具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中。
实例
假如客户需要在电商购买东西,流程如下
1、购买商品,客户付款,客户账户金额减少,调用Order API + Pay API;
2、MQ产生Order日志,状态Ordered;
3、商家减库存,调用Stock API;
4、MQ产生减库存Stock日志,状态Stocked;
5、商家收款,调用Order API + Pay API;
6、MQ产生Order日志,状态Finished;
现实情况会更复杂,这6个步骤只是简单处理。
本地事务为:1、3、5
MQ事务为:2、4、6
为了能解决该问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念。RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。
具体来说,就是把消息的发送分成了2个阶段:Prepare阶段和确认阶段。
具体来说,上面的2个步骤,被分解成3个步骤:
(1) 发送Prepared消息
(2) update DB
(3) 根据update DB结果成功或失败,Confirm或者取消Prepared消息。
事务监听
package com.xin.rocketmq.demo.testrun; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; public class TransactionListenerImpl implements TransactionListener { private AtomicInteger transactionIndex = new AtomicInteger(0); private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); int status = value % 3; localTrans.put(msg.getTransactionId(), status); //执行购买商品,客户付款,客户账户金额减少,调用Order API + Pay API; System.out.println("执行客户付款:" + msg.getTransactionId()); //商家减库存,调用Stock API; System.out.println("执行库存:" + msg.getTransactionId()); //商家收款,调用Order API + Pay API; System.out.println("执行商家收款:" + msg.getTransactionId()); return LocalTransactionState.UNKNOW; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { Integer status = localTrans.get(msg.getTransactionId()); if (null != status) { switch (status) { case 0: return LocalTransactionState.UNKNOW; case 1: return LocalTransactionState.COMMIT_MESSAGE; case 2: return LocalTransactionState.ROLLBACK_MESSAGE; } } return LocalTransactionState.COMMIT_MESSAGE; } }
事务生产者
package com.xin.rocketmq.demo.testrun; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.remoting.common.RemotingHelper; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.concurrent.*; public class TransactionProducer { public static void main(String[] args) throws MQClientException, InterruptedException { TransactionListener transactionListener = new TransactionListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("192.168.10.11:9876"); ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; } }); producer.setExecutorService(executorService); producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 2; i++) { try { Message msg = new Message("TopicTest1234", tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException | UnsupportedEncodingException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); } }
一批次处理2个订单,结果
SendResult [sendStatus=SEND_OK, msgId=A9FEC2CC2D8C18B4AAC2268193040000, offsetMsgId=null, messageQueue=MessageQueue [topic=TopicTest1234, brokerName=localhost.localdomain, queueId=3], queueOffset=103] 执行客户付款:A9FEC2CC2D8C18B4AAC2268193A20001 执行库存:A9FEC2CC2D8C18B4AAC2268193A20001 执行商家收款:A9FEC2CC2D8C18B4AAC2268193A20001 SendResult [sendStatus=SEND_OK, msgId=A9FEC2CC2D8C18B4AAC2268193A20001, offsetMsgId=null, messageQueue=MessageQueue [topic=TopicTest1234, brokerName=localhost.localdomain, queueId=0], queueOffset=104] 执行客户付款:A9FEC2CC2D8C18B4AAC2268193B10002 执行库存:A9FEC2CC2D8C18B4AAC2268193B10002 执行商家收款:A9FEC2CC2D8C18B4AAC2268193B10002
......
当发送半消息成功时,我们使用 executeLocalTransaction
方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTranscation
方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。
模拟两条记录全部成功走完事务
package com.xin.rocketmq.demo.testrun; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.System.*; public class TransactionListenerImpl implements TransactionListener { private AtomicInteger transactionIndex = new AtomicInteger(0); private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); int status = value % 3; localTrans.put(msg.getTransactionId(), status); //执行购买商品,客户付款,客户账户金额减少,调用Order API + Pay API; out.println("执行客户付款:" + msg.getTransactionId()); //商家减库存,调用Stock API; out.println("执行库存:" + msg.getTransactionId()); //商家收款,调用Order API + Pay API; out.println("执行商家收款:" + msg.getTransactionId()); return LocalTransactionState.COMMIT_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { return LocalTransactionState.COMMIT_MESSAGE; } }
开启消费者,发现两条事务都确实走完了。 执行客户付款:A9FEC2CC347C18B4AAC2269439D50000 执行库存:A9FEC2CC347C18B4AAC2269439D50000 执行商家收款:A9FEC2CC347C18B4AAC2269439D50000 SEND_OK -- SendResult [sendStatus=SEND_OK, msgId=A9FEC2CC347C18B4AAC2269439D50000, offsetMsgId=null, messageQueue=MessageQueue [topic=topic_family, brokerName=localhost.localdomain, queueId=2], queueOffset=257] 执行客户付款:A9FEC2CC347C18B4AAC226943A790001 执行库存:A9FEC2CC347C18B4AAC226943A790001 执行商家收款:A9FEC2CC347C18B4AAC226943A790001 SEND_OK -- SendResult [sendStatus=SEND_OK, msgId=A9FEC2CC347C18B4AAC226943A790001, offsetMsgId=null, messageQueue=MessageQueue [topic=topic_family, brokerName=localhost.localdomain, queueId=3], queueOffset=258] Receive message[msgId=A9FEC2CC347C18B4AAC2269439D50000] 86728ms later Receive message[msgId=A9FEC2CC347C18B4AAC226943A790001] 87048ms later
模拟1条记录在本地库存事务失败回滚
package com.xin.rocketmq.demo.testrun; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.System.*; public class TransactionListenerImpl implements TransactionListener { private AtomicInteger transactionIndex = new AtomicInteger(0); private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); int status = value % 3; localTrans.put(msg.getTransactionId(), status); //执行购买商品,客户付款,客户账户金额减少,调用Order API + Pay API; out.println("执行客户付款:" + msg.getTransactionId()); //商家减库存,调用Stock API; out.println("执行库存:" + msg.getTransactionId()); //商家收款,调用Order API + Pay API; if (status == 1)//第二个订单库存异常,模拟库存失败,需要回滚 return LocalTransactionState.ROLLBACK_MESSAGE; out.println("执行商家收款:" + msg.getTransactionId()); return LocalTransactionState.COMMIT_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { Integer status = localTrans.get(msg.getTransactionId()); if (null != status) { switch (status) { case 0: return LocalTransactionState.UNKNOW; case 1: return LocalTransactionState.ROLLBACK_MESSAGE; case 2: return LocalTransactionState.COMMIT_MESSAGE; } } return LocalTransactionState.COMMIT_MESSAGE; } }
两条事务,发现只有一条可以被成功消费。另一条回滚了。
Receive message[msgId=A9FEC2CC355818B4AAC2269681E00000]
目前维护的开源产品:https://gitee.com/475660