RocketMQ学习:事务消息
发表于2015 年 9 月 2 日由考拉哥
源代码版本是3.2.6,还是直接跑源代码。rocketmq事务消息是发生在Producer和Broker之间,是二阶段提交。
二阶段提交过程看图:

第一阶段是:步骤1,2,3。
第二阶段是:步骤4,5。
具体说明:
只有在消息发送成功,并且本地操作执行成功时,才发送提交事务消息,做事务提交。
其他的情况,例如消息发送失败,直接发送回滚消息,进行回滚,或者发送消息成功,但是执行本地操作失败,也是发送回滚消息,进行回滚。
事务消息原理实现过程:
一阶段:
Producer向Broker发送1条类型为TransactionPreparedType的消息,Broker接收消息保存在CommitLog中,然后返回消息的queueOffset和MessageId到Producer,MessageId包含有commitLogOffset(即消息在CommitLog中的偏移量,通过该变量可以直接定位到消息本身),由于该类型的消息在保存的时候,commitLogOffset没有被保存到consumerQueue中,此时客户端通过consumerQueue取不到commitLogOffset,所以该类型的消息无法被取到,导致不会被消费。
一阶段的过程中,Broker保存了1条消息。
二阶段:
Producer端的TransactionExecuterImpl执行本地操作,返回本地事务的状态,然后发送一条类型为TransactionCommitType或者TransactionRollbackType的消息到Broker确认提交或者回滚,Broker通过Request中的commitLogOffset,获取到上面状态为TransactionPreparedType的消息(简称消息A),然后重新构造一条与消息A内容相同的消息B,设置状态为TransactionCommitType或者TransactionRollbackType,然后保存。其中TransactionCommitType类型的,会放commitLogOffset到consumerQueue中,TransactionRollbackType类型的,消息体设置为空,不会放commitLogOffset到consumerQueue中。
二阶段的过程中,Broker也保存了1条消息。
总结:事务消息过程中,broker一共保存2条消息。
贴代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | < properties > < project.build.sourceEncoding >UTF-8</ project.build.sourceEncoding > < logback.version >1.0.13</ logback.version > < rocketmq.version >3.2.6</ rocketmq.version > </ properties > < dependencies > < dependency > < groupId >ch.qos.logback</ groupId > < artifactId >logback-classic</ artifactId > < version >1.0.13</ version > </ dependency > < dependency > < groupId >ch.qos.logback</ groupId > < artifactId >logback-core</ artifactId > < version >1.0.13</ version > </ dependency > < dependency > < groupId >com.alibaba.rocketmq</ groupId > < artifactId >rocketmq-client</ artifactId > < version >${rocketmq.version}</ version > </ dependency > < dependency > < groupId >junit</ groupId > < artifactId >junit</ artifactId > < version >4.10</ version > < scope >test</ scope > </ dependency > </ dependencies > |
TransactionCheckListenerImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package com.zoo.quickstart.transaction; import java.util.concurrent.atomic.AtomicInteger; import com.alibaba.rocketmq.client.producer.LocalTransactionState; import com.alibaba.rocketmq.client.producer.TransactionCheckListener; import com.alibaba.rocketmq.common.message.MessageExt; /** * 未决事务,服务器回查客户端,broker端发起请求代码没有被调用,所以此处代码可能没用。 */ public class TransactionCheckListenerImpl implements TransactionCheckListener { private AtomicInteger transactionIndex = new AtomicInteger( 0 ); @Override public LocalTransactionState checkLocalTransactionState(MessageExt msg) { System.out.println( "server checking TrMsg " + msg.toString()); int value = transactionIndex.getAndIncrement(); if ((value % 6 ) == 0 ) { throw new RuntimeException( "Could not find db" ); } else if ((value % 5 ) == 0 ) { return LocalTransactionState.ROLLBACK_MESSAGE; } else if ((value % 4 ) == 0 ) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.UNKNOW; } } |
本地操作类TransactionExecuterImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package com.zoo.quickstart.transaction; import java.util.concurrent.atomic.AtomicInteger; import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter; import com.alibaba.rocketmq.client.producer.LocalTransactionState; import com.alibaba.rocketmq.common.message.Message; /** * 执行本地事务 */ public class TransactionExecuterImpl implements LocalTransactionExecuter { private AtomicInteger transactionIndex = new AtomicInteger( 1 ); @Override public LocalTransactionState executeLocalTransactionBranch( final Message msg, final Object arg) { int value = transactionIndex.getAndIncrement(); if (value == 0 ) { throw new RuntimeException( "Could not find db" ); } else if ((value % 5 ) == 0 ) { return LocalTransactionState.ROLLBACK_MESSAGE; } else if ((value % 4 ) == 0 ) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.UNKNOW; } } |
Producer类:TransactionProducer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | package com.zoo.quickstart.transaction; import com.alibaba.rocketmq.client.exception.MQClientException; import com.alibaba.rocketmq.client.producer.SendResult; import com.alibaba.rocketmq.client.producer.TransactionCheckListener; import com.alibaba.rocketmq.client.producer.TransactionMQProducer; import com.alibaba.rocketmq.common.message.Message; /** * 发送事务消息例子 * */ public class TransactionProducer { public static void main(String[] args) throws MQClientException, InterruptedException { TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer( "please_rename_unique_group_name" ); // 事务回查最小并发数 producer.setCheckThreadPoolMinSize( 2 ); // 事务回查最大并发数 producer.setCheckThreadPoolMaxSize( 2 ); // 队列数 producer.setCheckRequestHoldMax( 2000 ); producer.setTransactionCheckListener(transactionCheckListener); producer.setNamesrvAddr( "192.168.0.104:9876" ); producer.start(); String[] tags = new String[] { "TagA" , "TagB" , "TagC" , "TagD" , "TagE" }; TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl(); for ( int i = 0 ; i < 1 ; i++) { try { Message msg = new Message( "TopicTest" , tags[i % tags.length], "KEY" + i, ( "Hello RocketMQ " + i).getBytes()); SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null ); System.out.println(sendResult); Thread.sleep( 10 ); } catch (MQClientException e) { e.printStackTrace(); } } for ( int i = 0 ; i < 100000 ; i++) { Thread.sleep( 1000 ); } producer.shutdown(); } } |
此条目发表在编程语言分类目录,贴了java, rocketmq, 两阶段事务提交标签。将固定链接加入收藏夹。
http://www.2cto.com/kf/201606/521538.html
下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?
RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:

总结:据笔者的了解,各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。比较遗憾的是,RocketMQ事务消息部分的代码也并未开源,需要自己去实现。
其他补偿方式
做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。
其实这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似。
一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,那么这种重试机制其实是非常有效的。
当然,考虑个比较极端的场景,假如系统自身有bug或者程序逻辑有问题,那么重试1W次那也是无济于事的。那岂不是就发生了“明明已经付款,却显示未付款不发货”类似的悲剧?
其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。
在某些特殊的情况下,还会有“人工补偿”的,这也是最后一道屏障。
小结
上诉的几种方案中,笔者也大致总结了其设计思路,优势,劣势等,相信读者已经有了一定的理解。其实分布式系统的事务一致性本身是一个技术难题,目前没有一种很简单很完美的方案能够应对所有场景。具体还是要使用者根据不同的业务场景去抉择。