前言
上篇文章介绍了RocketMQ事务消息的基本原理,对大致的流程及设计思路有个大致的了解
文章最后,也提出了几个问题
- 为什么prepare消息在发送后不会被消费?
- 事务消息又是如何提交、回滚的?
- 定时回查本地事务状态的机制又是怎么样?
废话不多说,直接盘源码,从源码中找问题答案,源码中都加了todo注释,方便大家查看
发送流程
先从事务消息发送方法入手TransactionMQProducer#sendMessageInTransaction
进到DefaultMQProducerImpl#sendMessageInTransaction方法
这是一个典型的两阶段提交过程 步骤1、2为第一阶段,步骤3为第二阶段
1、producer发送prepare消息给mq服务器
2、如果消息发送成功,执行本地事务,同时将消息transactionId与业务操作一并入库,方便后续事务回查
3、根据本地事务执行状态,决定是否对prepare消息进行提交/回滚,本地事务执行状态有下面几种
这时候再反过来看事务消息原理设计图,是不是清晰很多
方法中还剩两个关键方法
- this.send(msg)
- this.endTransaction(sendResult, localTransactionState, localException)
下面逐个分析,先看消息发送send()方法
事务消息发送
事务消息的发送,会执行到DefaultMQProducerImpl#sendDefaultImpl方法,再调用DefaultMQProducerImpl#sendKernelImpl方法发送消息,下面只贴出与事务消息相关代码
TRANSACTION_PREPARED_TYPE类型即半消息类型(prepare消息)
在消息发送之前,进到SendMessageProcessor#sendMessage方法,包含如下代码
事务消息与普通消息发送基本没区别,只是给消息属性PROPERTY_TRANSACTION_PREPARED
打了个标记true,后续事务存储也是根据该标记进行特殊处理
事务消息存储
上面代码已经看到,TransactionalMessageService#prepareMessage方法是对事务消息进行存储的方法,看下代码
会调用TransactionalMessageBridge#putHalfMessage进行消息存储
关键点在于parseHalfMessageInner
方法
注释里写的很清楚,核心就是两点
- 修改消息topic为RMQ_SYS_TRANS_HALF_TOPIC,并备份消息原有topic,供后续commit消息时还原消息topic使用
- 修改消息queueId为0,并备份消息原有queueId,供后续commit消息时还原消息queueId使用
看到这,也就回答了第一个问题 为什么prepare消息在发送后不会被消费?
修改完topic和queueId后,事务消息也会像普通消息一样存储在commitLog中
消息事务提交/回滚
RMQ_SYS_TRANS_HALF_TOPIC对应队列,后面简称halfQueue
RMQ_SYS_TRANS_OP_HALF_TOPIC对应队列,后面简称opQueue
回到发送流程中提到的this.endTransaction方法,在本地事务执行后,会根据LocalTransactionState来向mq服务器发送不同的请求,来看下具体代码
对事务结束请求EndTransactionRequestHeader的处理方法在EndTransactionProcessor#processRequest方法中,看下关键逻辑
提交
提交消息的分为四个步骤
- 根据commitLog查询对应事务消息,对应TransactionalMessageService#commitMessage方法
- 从消息属性 PROPERTY_REAL_TOPIC 及 PROPERTY_REAL_QUEUE_ID 中,取出并恢复消息原来的 topic,queueId,对应endMessageTransaction方法
看下代码
- 调用 MessageStore#putMessage 将还原后的消息存储到commitLog中,对应sendFinalMessage方法
- 删除消息(将消息移到topic为RMQ_SYS_TRANS_OP_HALF_TOPIC队列中),对应TransactionalMessageService#deletePrepareMessage方法
看下实现
最终调用了TransactionalMessageBridge#addRemoveTagInTransactionOp方法
核心点在于
- 初始化了一个Top为RMQ_SYS_TRANS_OP_HALF_TOPIC的消息
- 消息体为半消息队列的offSet,方便索引到半消息队列中的消息,这里是回查机制设计的核心,下面会提到
- halfQueue中处理过的消息,其offSet才会存在于opQueue的消息体中,后续消息回查时会用到
回滚
直接对应提交消息中的第四步,调用TransactionalMessageService#deletePrepareMessage方法,将消息put到topic为RMQ_SYS_TRANS_OP_HALF_TOPIC的队列中
看到这,对消息提交、回滚做了说明,无论消息是提交还是回滚,消息都会被put到topic为RMQ_SYS_TRANS_OP_HALF_TOPIC的队列中,总结下两个Topic的作用
RMQ_SYS_TRANS_HALF_TOPIC:prepare消息的主题,事务消息首先先进入到该主题。
RMQ_SYS_TRANS_OP_HALF_TOPIC:当消息服务器收到事务消息的提交或回滚请求后,会将消息存储在该主题下
开头的第二个问题 事务消息又是如何提交、回滚的? 也迎刃而解
那么还有第三个问题 定时回查本地事务状态的机制又是怎么样?
事务消息回查
事务消息回查的实现在TransactionalMessageCheckService中,代码如下
详细分析下TransactionalMessageService#check方法
再来看下for循环中处理逻辑,由于代码较长,分段截取并注释说明
1、先从halfQueue,opQueue中取出对应offSet
2、根据halfQueue,opQueue判断出opQueue中哪些消息已经处理过,哪些没处理过
处理过的opQueue offSet放入doneOpOffset中
具体实现逻辑如下,核心就是拿opQueue中消息体内的halfQueue的offSet(已经处理过的半消息offSet)与当前halfQueue的offSet做比较
3、接下来就到了消息回查的实现关键,先看下整体流程
核心回查在else中
4、消息拉取逻辑,判断回查次数、存储时间逻辑如下
5、判断是否该条消息存储时间,是否超过了立即执行回查的超时时间checkImmunityTime,没超时则不执行回查,逻辑如下
6、重置消息消费位点queueOffSet,putBackHalfMsgQueue
首先判断是否需要进行消息事务状态回查,isNeedCheck
valueOfCurrentMinusBorn > checkImmunityTime,这个逻辑很好理解,当前消息存储时间已经超过了超时时间,应当进行事务回查
还有个逻辑是判断当前获取的最后一条OpMsg的存储时间是否超过了事务超时时间,如果为true也要进行事务状态回查,为什么要这么做呢?
因为在下文中,如果isNeedCheck=true,会调用putBackHalfMsgQueue重新将opMsg放入opQueue中,重新放入的消息被重置了queueOffSet,commitLogOffSet,即将消费位点前移了,放到opQueue最新一条消息中
所以
如果事务状态回查成功,则fillOpRemoveMap会使得doneOpOffset包含该halfQueue offSet,即使消费位点前移了,后续也不会再重复处理
如果事务状态回查失败,则判断拉取到的32条消息的最新一条消息存储时间是否超过超时时间,如果是,那肯定是回查失败的,继续进行回查
7、具体事务回查代码逻辑AbstractTransactionalMessageCheckListener#resolveHalfMsg中,启动了一个线程池来进行事务回查
8、再看下sendCheckMessage方法
9、接收事务回查请求逻辑在ClientRemotingProcessor#checkTransactionState中,各种请求的接受者的查询逻辑可以根据RequestCode中的变量值来找,看下具体代码
至于checkTransactionState,与事务提交部分代码较为相似,只不过这里是调用TransactionListener#checkLocalTransaction来得到事务执行状态码localTransactionState,再根据localTransactionState来发送commit/rollback请求,不再赘述
至此,第三个问题 定时回查本地事务状态的机制又是怎么样也解决了