整个业务流程如下图:
业务流程
下面分析下为什么需要使用RocketMQ的事务消息,而不使用普通消息。
场景一:财务模块先入库,提交事务后发送普通消息。
存在问题:数据入库成功,如果消息发送失败了,导致财务模块和清结算模块数据不一致了。
场景二:财务模块先发送普通消息,普通消息发送成功后再提交本地事务。
存在问题:如果普通消息发送成功了,但是财务模块本地事务提交失败了,同样也会导致财务模块和清结算模块数据不一致了。
正是由于以上问题存在,所以我们需要使用RocketMQ的事务消息,来保证数据的最终一致性,其实这也是解决分布式事务最常用的一种方法,下图是RocketMQ事务消息交互流程图:
RocketMQ事务消息交互流程
问题分析
下面来分析文章开头说的问题
问题一:财务模块本地事务执行异常,并没有抛出异常,导致财务模块接收普通消息后正常ack了,然后导致财务数据生成异常,导致数据丢失。
问题二:财务模块本地事务执行异常后,RocketMQ执行回查本地事务状态后,明明返回的事务状态为TransactionStatus.Unknow,但是却将事务消息提交了,导致清结算模块接收到了该消息,从而导致了数据不一致。
我们先对问题一进行分析,首先看下业务代码:
业务代码调用半事务消息发送
正如图片描述的,当本地事务出现异常后,并没有打印出异常信息,然后跟踪事务消息发送代码如下,核心类:TransactionProducerImpl::send(final Message message, final LocalTransactionExecuter executer, Object arg)
半事务消息发送
那么接下来定位到类TransactionMQProducer::sendMessageInTransaction(final Message msg, final LocalTransactionExecuter tranExecuter, final Object arg)
真正发送半事务消息的方法
从上面的代码我们可以发现,事务消息如果没有设置回查本地事务的方法的话,会报错,好的,我们继续向下跟踪代码,真正发送半事务消息的方法类DefaultMQProducerImpl::sendMessageInTransaction(final Message msg,final LocalTransactionExecuter tranExecuter, final Object arg)
半事务消息发送完成,并返回结果
半事务消息发送完成之后,则会根据具体的事务消息返回结果执行回调本地事务的函数,如下:
半事务消息发送成功回调本地事务方法
从上面代码我们可以发现,当发送完半事务消息成功后,执行本地事务如果发生异常,会被catch起来进行错误信息包装,但是翻看前面代码类:TransactionProducerImpl::send(final Message message, final LocalTransactionExecuter executer, Object arg)
最开始半事务消息发送
我们会发现阿里云ons进行代码封装后,并没有将异常信息返回给我们,而只是返回给了我们半事务消息发送的结果,这也就解释了问题一:财务模块本地事务执行异常,并没有抛出异常。所以我们的回查代码应该按照以下这种方式写
业务异常捕获
接下来我们对问题二进行排查,因为本地事务消息回查是broker服务器端向客户端进行触发端,所以我们需要下载RocketMQ开源版端代码进行跟踪,经过跟踪我们排查到了类TransactionalMessageServiceImpl::check(long transactionTimeout, int transactionCheckMax, AbstractTransactionalMessageCheckListener listener) 由于上面这个方法含有的代码篇幅过长,就不进行复制了,主要逻辑就是在半事务消息的Topic:RMQ_SYS_TRANS_HALF_TOPIC获取没有提交或者回滚的半事务消息,然后进行回调检查,该方法中比较核心的检查方法为:AbstractTransactionalMessageCheckListener#resolveHalfMsg
broker调用回查方法定时任务
然后我们继续点sendCheckMessage方法跟踪到类AbstractTransactionalMessageCheckListener::sendCheckMessage(MessageExt msgExt) 的回调方法,发现了其中一个重要的代码
半事务消息回调逻辑
从以上代码我们发现,半事务消息在进行本地事务消息回查的过程中需要使用事务消息发送者的groupId,然后根据groupId再获取Netty的channel对象才能进行本地事务消息回查,正是由于我们在生产者事务消息发送时没有设置生产者的Group,而导致的本地事务消息回查异常,当本地事务消息发送异常了,也会导致消息被发送,至此,问题已经在源码层面得到了答案!
不断分享开发过程用到的技术和面试经常被问到的问题,如果您也对IT技术比较感兴趣可以「关注」我,让我们共同学习,共同进步!