RocketMQ从4.3.0版本开始支持分布式事务消息,其采用了2PC的思想实现了提交事务消息,同时增加一个补偿逻辑处理二阶段超时或者失败的消息。本篇文章会从发送事务消息及事务消息回查来分析事务消息实现的原理,最后会通过实际的例子来讲解事务消息在RocketMQ中的应用。
事务消息整体流程
首先先来看看在RocketMQ中事务消息的整体流程,如下图所示:
1.producer发送消息到broker
2.broker将消息存储并返回给producer响应
3.执行producer本地事务并返回事务状态
4.根据事务状态提交或者回滚事务
5.对没有提交或者回滚的消息,broker发起回查请求
6.producer收到回查请求后检查消息对应的事务状态
7.根据回查的事务状态重新提交或者回滚消息
这里先介绍几个有关事务消息的术语:
1.在事务消息的整体流程中第一步发送给broker的消息通常被称为半消息(half消息)或者prepare消息
2.事务消息的状态分为三种分别是:
- COMMIT_MESSAGE(提交事务)
- ROLLBACK_MESSAGE(回滚事务)
- UNKNOW(需要回查的事务)
3.存储在"RMQ_SYS_TRANS_HALF_TOPIC" topic的消息被称为半消息,存储在"RMQ_SYS_TRANS_OP_HALF_TOPIC" topic中的消息被称为op消息
4.RMQ_SYS_TRANS_HALF_TOPIC和RMQ_SYS_TRANS_OP_HALF_TOPIC这两个topic是系统级别的topic,它们对consumer是不可见的
事务消息发送流程
事务消息的整个流程是从producer发送消息开始,我们就从sendMessageInTransaction方法入手。在这个方法中会先判断transactionListener是否为空,如果为空则会抛出异常TransactionListener is null。TransactionListener是事务监听器,其定义了实现本地事务执行和本地事务状态回查两个接口。应用在使用RocketMQ时,如果涉及到事务消息需要在producer端实现TransactionListener的executeLocalTransaction和checkLocalTransaction两个接口。
public TransactionSendResult sendMessageInTransaction(final Message msg,
final Object arg) throws MQClientException {
if (null == this.transactionListener) {
throw new MQClientException("TransactionListener is null", null);
}
msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
事务消息发送流程的核心实现是在sendMessageInTransaction方法中:
1.判断transactionListener和localTransactionExecuter是否同时为null,如果同时为空则抛出异常tranExecutor is null
2.判断消息扩展属性DELAY是否为0,如果不为0则将DELAY从消息的扩展属性中删除(这里看出事务消息是不支持延迟消息)
3.检查消息的有效性
4.在消息的扩展属性中添加TRAN_MSG属性,其值为true,这表示该消息为prepare消息
5.在消息的扩展属性中添加PGROUP属性,其值为生产者所属组,设置PGROUP的原因在于后续回查事务状态时从生产者组中随机选择一个生产者向其发送事务回查请求
6.将prepare消息发送给broker,这个过程与同步发送普通消息是同一个函数,但是发送事务消息与普通消息还是有区别的:
(1)在发送前,如果是事务消息会计算sysFlag并将其设置到发送消息的请求中
(2)broker端在接受到请求后在对消息进行存储时会先对消息进行判断,如果是事务消息则会调用transactionalMessageService服务的asyncPrepareMessage方法来完成事务消息存储
这里看下broker在收到producer端发送的RequestCode.SEND_MESSAGE请求后是如何存储prepare消息的。asyncPrepareMessage方法会先调用parseHalfMessageInner方法对prepare消息进行处理:
(1)将prepare消息真正的topic存储在REAL_TOPIC扩展属性
(2)将prepare消息的queueId存储在REAL_QID扩展属性
(3)修改prepare消息的topic设置为RMQ_SYS_TRANS_HALF_TOPIC
(4)修改prepare消息的queueId设置为0
在对prepare消息进行改造后就是调用asyncPutMessage将消息存储到commitlog中,这里需要注意由于消息是存储在RMQ_SYS_TRANS_HALF_TOPIC中,所以消息是不会被consumer消费的
7.如果prepare消息在broker存储成功则调用executeLocalTransaction方法完成本地事务并将事务状态记录在localTransactionState
8.调用endTransaction方法构建EndTransactionRequestHeader请求并将步骤7中事务消息的状态设置到请求的commitOrRollback属性中,最后将请求RequestCode.END_TRANSACTION发送到broker。这里需要知道事务状态与请求中commitOrRollback的对应关系。
事务状态 | commitOrRollback |
---|---|
COMMIT_MESSAGE | TRANSACTION_COMMIT_TYPE |
ROLLBACK_MESSAGE | TRANSACTION_ROLLBACK_TYPE |
UNKNOW | TRANSACTION_NOT_TYPE |
broker端在收到RequestCode.END_TRANSACTION请求后,是在EndTransactionProcessor的processRequest方法中处理请求,在该方法中会根据请求中commitOrRollback设置的事务的状态分别进行处理:
(1)如果是TRANSACTION_COMMIT_TYPE,首先会根据请求中的offset从commitlog中获取prepare消息,然后根据prepare消息构建一个新的MessageExtBrokerInner对象,新消息的topic和queueId是prepare消息扩展属性中REAL_TOPIC和REAL_QID对应的值,最后将新消息存储到其实际的topic中,此时消息对consumer是可见的。在新消息发送成功后还会根据prepare消息构造op消息并将op消息存储在commitlog中,op消息与prepare消息的区别在于其topic是RMQ_SYS_TRANS_OP_HALF_TOPIC,消息体内容是prepare消息在consumequeue的偏移量。
(2)如果是TRANSACTION_ROLLBACK_TYPE,首先会请求中的offset从commitlog中获取prepare消息,然后根据prepare消息构建op消息,最后将op消息存储在commitlog中
(3)如果是TRANSACTION_NOT_TYPE则不做任何处理
从上面可以看出broker会在RMQ_SYS_TRANS_OP_HALF_TOPIC中存储commit或者rollback状态的消息,也就是已经处理过的消息,op消息存在意义就是在事务回查时判断哪些消息需要回查
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
// ignore DelayTimeLevel parameter
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
Validators.checkMessage(msg, this.defaultMQProducer);
SendResult sendResult = null;
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
case SEND_OK: {
try {
if (sendResult.getTransactionId() != null) {
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !"".equals(transactionId)) {
msg.setTransactionId(transactionId);
}
if (null != localTransactionExecuter) {
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
log.debug("Used new transaction API");
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
log.info("executeLocalTransactionBranch return {}", localTransactionState);
log.info(msg.toString());
}
} catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
localException = e;
}
}
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
try {
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
TransactionSendResult transactionSendResult = new TransactionSendResult();
transactionSendResult.setSendStatus(sendResult.getSendStatus());
transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
transactionSendResult.setMsgId(sendResult.getMsgId());
transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
transactionSendResult.setTransactionId(sendResult.getTransactionId());
transactionSendResult.setLocalTransactionState(localTransactionState);
return transactionSendResult;
}
事务消息回查流程
从上面事务消息发送流程可以看到broker端没有对UNKNOW状态的消息进行处理,事务消息的回查流程就是处理UNKNOW状态的消息,其实现的方式是通过一个定时任务将UNKNOW状态的消息发送到producer端,producer端通过执行checkLocalTransaction方法来检查事务的状态并根据事务状态再次发送RequestCode.END_TRANSACTION请求到broker,broker根据事务状态分别处理与上面的步骤一样。需要注意的是事务回查次数最多是15次。TransactionalMessageCheckService用来服务事务回查,其本质是一个线程,checkInterval的默认值是60秒,这个可以在配置文件中配置,配置项是:transactionCheckInterval,单位是毫秒。以下是事务回查的核心实现:
public void check(long transactionTimeout, int transactionCheckMax,
AbstractTransactionalMessageCheckListener listener) {
try {
String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
//获取RMQ_SYS_TRANS_HALF_TOPIC这个topic的所有MessageQueue集合msgQueues
Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
if (msgQueues == null || msgQueues.size() == 0) {
log.warn("The queue of topic is empty :" + topic);
return;
}
log.debug("Check topic={}, queues={}", topic, msgQueues);
/*
*遍历msgQueues
*/
for (MessageQueue messageQueue : msgQueues) {
long startTime = System.currentTimeMillis();
//opQueueMap的结构是ConcurrentHashMap<MessageQueue, MessageQueue>,其中存储的是RMQ_SYS_TRANS_HALF_TOPIC的MessageQueue及RMQ_SYS_TRANS_OP_HALF_TOPIC的MessageQueue
MessageQueue opQueue = getOpQueue(messageQueue);
//获取messagequeue的消费进度
long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
//获取opQueue的消费进度
long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
//如果两个消费进度中的任何一个小于0则忽略该消息队列,继续处理下一个消息队列
if (halfOffset < 0 || opOffset < 0) {
log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
halfOffset, opOffset);
continue;
}
List<Long> doneOpOffset = new ArrayList<>();
HashMap<Long, Long> removeMap = new HashMap<>();
//fillOpRemoveMap方法实现的功能是构造removeMap和doneOpOffset
PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);
if (null == pullResult) {
log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null",
messageQueue, halfOffset, opOffset);
continue;
}
// single thread
int getMessageNullCount = 1;
long newOffset = halfOffset;
long i = halfOffset;
while (true) {
if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
break;
}
if (removeMap.containsKey(i)) {//说明消息已经被处理过了
log.info("Half offset {} has been committed/rolled back", i);
Long removedOpOffset = removeMap.remove(i);
doneOpOffset.add(removedOpOffset);
} else {
//获取半消息
GetResult getResult = getHalfMsg(messageQueue, i);
MessageExt msgExt = getResult.getMsg();
if (msgExt == null) {
if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {
break;
}
if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {
log.debug("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i,
messageQueue, getMessageNullCount, getResult.getPullResult());
break;
} else {
log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}",
i, messageQueue, getMessageNullCount, getResult.getPullResult());
i = getResult.getPullResult().getNextBeginOffset();
newOffset = i;
continue;
}
}
//判断消息是否被忽略
//transactionCheckMax是事务回查最大检测次数,needDiscard函数实现的功能是比较消息回查次数与transactionCheckMax的大小,如果回查次数大于等于transactionCheckMax则返回true表示该消息被丢弃,否则将消息回查次数加1并返回false,消息回查次数被记录在消息的扩展属性中,其key是TRANSACTION_CHECK_TIMES
//needSkip函数实现的功能是判断消息存储时间是否大于文件的过期时间,如果大于则返回true否则返回false
//满足needDiscard函数或者needSkip函数则执行resolveDiscardMsg函数将消息存储到TRANS_CHECK_MAX_TIME_TOPIC中(实现方式还是根据消息构建一个新的MessageExtBrokerInner对象,但是其topic名称是TRANS_CHECK_MAX_TIME_TOPIC,实现函数是toMessageExtBrokerInner),并更新newOffset及i继续下条消息的判断。在该MessageQueue的检查逻辑执行完之后会更新offset,之后不会对其进行回查
if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
listener.resolveDiscardMsg(msgExt);
newOffset = i + 1;
i++;
continue;
}
//在检查半消息的messagequeue后,此时可能会有半消息写入;因为ConsumeQueue是按顺序构建的所以该messagequeue后面的消息也不用检查了
if (msgExt.getStoreTimestamp() >= startTime) {
log.debug("Fresh stored. the miss offset={}, check it later, store={}", i,
new Date(msgExt.getStoreTimestamp()));
break;
}
//消息已经存储的时间(系统当前时间与消息存储的时间差)
long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
//transactionTimeout是事务消息的过期时间,checkImmunityTime是指从发送事务消息成功后到应用程序提交事务的时间这段时间,因为在这段时间内事务没有提交,所以不应该在这段时间内向应用程序发起事务回查请求
long checkImmunityTime = transactionTimeout;
//checkImmunityTimeStr指的是事务消息的有效时间,只有在这个时间段内收到回查消息才有效
String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
if (null != checkImmunityTimeStr) {
//getImmunityTime函数的功能是:如果checkImmunityTimeStr为-1则将transactionTimeout赋值给checkImmunityTimeStr并返回,否则则返回checkImmunityTimeStr*1000
checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
if (valueOfCurrentMinusBorn < checkImmunityTime) {
//checkPrepareQueueOffset函数返回true表示该消息会被跳过
if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
newOffset = i + 1;
i++;
continue;
}
}
} else {//如果生产者没有checkImmunityTimeStr,此时如果消息已经存储的时间小于checkImmunityTime,则跳出该半消息及后续所有的半消息(因为同一个ConsumeQueue中消息是顺序的)
if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
checkImmunityTime, new Date(msgExt.getBornTimestamp()));
break;
}
}
List<MessageExt> opMsg = pullResult.getMsgFoundList();
//判断消息是否需要回查,满足以下3个条件中的其中之一都需要回查
//(1)RMQ_SYS_TRANS_OP_HALF_TOPIC中没有已处理的消息并且半消息已经存储的时间大于checkImmunityTime
//(2)从RMQ_SYS_TRANS_OP_HALF_TOPIC的上次消费位点开始往后还有消息且获取到的最后一条消息的存储时间大于transactionTimeout
//(3)valueOfCurrentMinusBorn小于等于-1
boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
|| (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
|| (valueOfCurrentMinusBorn <= -1);
if (isNeedCheck) {
//在消息回查前将半消息再次写入commitlog中
if (!putBackHalfMsgQueue(msgExt, i)) {
continue;
}
//resolveHalfMsg方法的核心是调用了sendCheckMessage方法,注意:这里是异步处理
//sendCheckMessage方法主要完成以下任务:1.构造CheckTransactionStateRequestHeader 2.将msgExt的topic和queueId设置为真实的topic和queueId 3.获取消息的groupId并从中随机选择一个producer 4.向producer发送一个RequestCode.CHECK_TRANSACTION_STATE类型的请求(发送方式是oneway)
listener.resolveHalfMsg(msgExt);
} else {
pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
log.debug("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
messageQueue, pullResult);
continue;
}
}
newOffset = i + 1;
i++;
}
//更新prepare消息的回查进度
if (newOffset != halfOffset) {
transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
}
//计算op消息的consumequeue需要往前推进的进度
long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
//更新op消息的进度
if (newOpOffset != opOffset) {
transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
}
}
} catch (Throwable e) {
log.error("Check error", e);
}
}
下图是事务消息中producer与broker之间的交互图方便大家阅读源码:
事务消息例子
在学习事务这个概念时通常都会举转账的例子,这里我们就分析这个经典的例子:张三给李四转账100元(假设张三发送的topic是A,李四订阅了A),整个过程如下:
1.张三将消息“李四的账户添加100元”发送到Broker
2.Broker收到消息后会将消息存储在RMQ_SYS_TRANS_HALF_TOPIC中(李四看不到这个topic)
3.Broker将消息存储成功后会返回一个OK响应给张三,张三收到响应后知道消息发送成功后会执行本地事务操作:张三的账户减少100元
4.张三执行完本地事务后会发送结束事务的请求给Broker,该请求包含了本地事务的状态
5.Broker收到请求后会查看请求中张三执行本地事务的结果,张三执行的结果可以分为三种:
(1)成功:创建消息“李四的账户添加100元”对应的op消息并将消息“李四的账户添加100元”存储到topic A中(此时李四能够看到消息)
(2)失败:创建消息“李四的账户添加100元”对应的op消息,此时消息对李四是不可见的,所以李四的账户不会增加100元
(3)因为网络等原因没有返回张三执行本地事务的结果(此时不做处理,会有回查)
6.如果张三执行本地事务成功,则李四会在topic A中看到消息“李四的账户添加100元”,接着会执行操作:李四的账户添加100元
如果没有返回张三执行本地事务的结果,那么Broker端会发起回查,发送请求将待确认状态的消息发给张三,然后张三检查消息的状态,最后发送给Broker结束事务的请求。Broker收到结束事务请求后会执行上面的步骤5。需要注意的是回查的次数不超过15次。张三、李四及RocketMQ Broker之间的交互图如下:
最后再来看看看事务不一致的情况会不会发生:
第一种情况:张三的账户没有减少100元,李四的账户成功的增加了100元
第二种情况:张三的账户成功的减少了100元,李四的账户没有增加100元
第一种情况在RocketMQ中是不会发生的,因为张三的账户没有减少100元(producer端执行本地事务失败)的情况下,消息在RocketMQ的系统级别的topic中,对李四(consumer)是不可见的;第二种情况是有可能发生的,因为RocketMQ只是保证了张三在成功的完成本地事务的情况下,消息“李四的账户添加100元”是能够正常被李四看到,至于李四获取到消息后有没有成功的将李四的账户增加100元张三是不管的,这种情况下可以依靠consumer的消费重试机制来保证,所以RocketMQ中实现的是事务的最终一致性。