RocketMQ事务消息

之前提到过,我们自研的消息队列中间件参考了RocketMQ的实现,但是有的细节上考虑得不是很全面。所以最近又拿我们的消息队列与RocketMQ做一些对比,看看有哪些点是当时没理解透,而做出的一些不恰当改动。

恰好想到我们还不支持事务消息,而RocletMQ在2018年4.3版本发布时,就已经支持了事务消息。

详细可以参考文章:https://mp.weixin.qq.com/s/43wwC4lp77m4foVPEgTRlA

作为一个从业中间件开发的低级开发人员,这篇文章显然不能满足我的好奇心,索性读一遍源码,为以后我们支持事务消息做一些前期调研。

先对上述文章做一个简单的总结:

RocketMQ采用了2pc的方式来实现事务消息,客户端首先发送prepare类型消息,采用替换主题号的方式,保存到服务端,保证该prepare不会被消费端消费到。服务端定时会去发送客户端检查该prepare消息的状态,根据状态服务端决定是commit该消息还是回滚该消息,还是不做处理等待下次检查。如果是commit 操作,则会把propare消息恢复原主题号重新保存到commitlog中,并创建operation消息,用以标识该prepare消息事务已经结束。如果是rollback操作,只会创建operation消息,标识该prepare消息事务结束。这就保证了只有commit的消息可以被消费到,并且服务端的回查机制保证了事务消息的可靠性。

我们先来看客户端是如何做的,首先我看看一个事务消息发送的例子:

public void testPush() throws MQClientException {
        TransactionMQProducer producer = new TransactionMQProducer();
        producer.setProducerGroup(getProducerGroupName());
        producer.setInstanceName(getProducerInstanceName());
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                return null;
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                return null;
            }
        });
        producer.start();
}

可以看到这里有两个关键的类,一个是事务消息的发送客户端TransactionMQProducer,一个是事务监听器TransactionListener。

我们先看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);
    }

这里看到如果我们没有设置事务监听器,则会直接抛出异常。实际调用的还是DefaultMQProducerImpl的sendMessageInTransaction方法。

send方法太长,我们分段来看,

第一步:是发送前的属性设置和校验

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());
		

首先依旧是判断监听器是否存在,这里LocalTransactionExecuter就是第二个参数,这个类已经废弃了,被TransactionListener 所代替了。接着是清楚了延迟消息的属性,检验消息合法性,包括主题号合法性、消息体合法性等。接着设置了该消息为prepare类型消息,并且设置消息所属的生产组,用来回查消息时使用。

第二步:发送消息和发送结果的判断

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;
}

如果发送成功,消息设置了属性PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX,则把该值作为消息的事务id,并且调用了TransactionListener的executeLocalTransaction方法,来执行本地事务。

如果消息发送失败,则设置事务状态为ROLLBACK_MESSAGE.

第三步:消息发送结束,根据本地事务状态来进行提交或者回滚

try {
	this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
	log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}

这里会根据发送的结果、本地事务结果来结束事务消息。

public void endTransaction(
			final SendResult sendResult,
			final LocalTransactionState localTransactionState,
			final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
		final MessageId id;
		if (sendResult.getOffsetMsgId() != null) {
			id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
		} else {
			id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
		}
		String transactionId = sendResult.getTransactionId();
		final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
		EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
		requestHeader.setTransactionId(transactionId);
		requestHeader.setCommitLogOffset(id.getOffset());
		switch (localTransactionState) {
			case COMMIT_MESSAGE:
				requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
				break;
			case ROLLBACK_MESSAGE:
				requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
				break;
			case UNKNOW:
				requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
				break;
			default:
				break;
		}

		requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
		requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
		requestHeader.setMsgId(sendResult.getMsgId());
		String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
		this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
				this.defaultMQProducer.getSendMsgTimeout());
}

这里根据你实际的事务状态,来组装消息发送到服务端。可以看到这里会设置标识位,表明该回复消息来自于服务端的check,该标识服务端会用到来区分消息来源,用来标记日志。

以上是客户端发送事务消息的整体流程,还差一部分内容,就是事务状态回查是如何调用的。

当客户端收到服务端发起的回查请求--请求编码为CHECK_TRANSACTION_STATE时,执行check方法,这里就不再贴源码了,简单说一下就是要构造一个异步任务,调用TransactionListener的checkLocalTransaction方法来获取事务的状态,然后根据事务状态构造返回信息给服务端。

 

接下来看服务端,当服务端消息时,会对消息进行判断是否为prepare消息:

if (transFlag != null && Boolean.parseBoolean(transFlag)) {
      if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
           response.setCode(ResponseCode.NO_PERMISSION);
           response.setRemark(
                    "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                                + "] sending transaction message is forbidden");
           return CompletableFuture.completedFuture(response);
      }
      putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
} else {
      putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
}

如果是prepare类型的消息,则会调用TransactionMessageService的asyncPrepaseMessage方法,该方法最终也是调用了MessageStore的putMessage方法,把消息保存到commitlog中,但是中间对消息做了特殊处理:

private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
            String.valueOf(msgInner.getQueueId()));
        msgInner.setSysFlag(
            MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
        msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
        msgInner.setQueueId(0);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        return msgInner;
    }

采用了RocketMQ一贯的替换主题号与队列id的做法,隐藏了原有的主题号与队列id,替换为prepare消息特有的主题号,这样该消息就不能被消费端消费到。

当收到事务结束消息时,服务端会有EndTransactionProcessor来处理,标记日志等内容忽略,我们只看关键部分:

OperationResult result = new OperationResult();
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
    result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
    if (result.getResponseCode() == ResponseCode.SUCCESS) {
        RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
        if (res.getCode() == ResponseCode.SUCCESS) {
            MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
                           
          msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
                                  
          msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
                        
          msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
                    
          msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
          MessageAccessor.clearProperty(msgInner, 
          MessageConst.PROPERTY_TRANSACTION_PREPARED);
          RemotingCommand sendResult = sendFinalMessage(msgInner);
          if (sendResult.getCode() == ResponseCode.SUCCESS) {
                            
 this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
          }
          return sendResult;
        }
        return res;
    }
} 
else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
        result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
        if (result.getResponseCode() == ResponseCode.SUCCESS) {
            RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
            if (res.getCode() == ResponseCode.SUCCESS) {
                    this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
            }
        return res;
        }
}

我们看到无论是commit还是rollback,同样都调用了deletePrepareMessage方法,只不过commit先调用了sendFinalMessage方法。

我们先看commit的特殊之处,首先是创建事务结束消息,endMessageTransaction方法:

 private MessageExtBrokerInner endMessageTransaction(MessageExt msgExt) {
        MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
        msgInner.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
        msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
        msgInner.setBody(msgExt.getBody());
        msgInner.setFlag(msgExt.getFlag());
        msgInner.setBornTimestamp(msgExt.getBornTimestamp());
        msgInner.setBornHost(msgExt.getBornHost());
        msgInner.setStoreHost(msgExt.getStoreHost());
        msgInner.setReconsumeTimes(msgExt.getReconsumeTimes());
        msgInner.setWaitStoreMsgOK(false);
        msgInner.setTransactionId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
        msgInner.setSysFlag(msgExt.getSysFlag());
        TopicFilterType topicFilterType =
            (msgInner.getSysFlag() & MessageSysFlag.MULTI_TAGS_FLAG) == MessageSysFlag.MULTI_TAGS_FLAG ? TopicFilterType.MULTI_TAG
                : TopicFilterType.SINGLE_TAG;
        long tagsCodeValue = MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags());
        msgInner.setTagsCode(tagsCodeValue);
        MessageAccessor.setProperties(msgInner, msgExt.getProperties());
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
        MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC);
        MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID);
        return msgInner;
    }

还是RocketMQ的一贯做法,恢复原本的主题号与队列id,清空没用的property属性。然后保存该消息到commitlog中,由于恢复了主题号,所以该消息就可以被正确的消费到了。

我们解析来看结束事务的方法,这里没有对prepare消息状态进行修改,因为RocketMQ都是顺序append写文件的,所以这里采用了再写一条operation消息,来标识该prepare消息已经结束的状态。实际调用如下两个方法

private boolean addRemoveTagInTransactionOp(MessageExt messageExt, MessageQueue messageQueue) {
        Message message = new Message(TransactionalMessageUtil.buildOpTopic(),     TransactionalMessageUtil.REMOVETAG,
            String.valueOf(messageExt.getQueueOffset()).getBytes(TransactionalMessageUtil.charset));
        writeOp(message, messageQueue);
        return true;
}

private void writeOp(Message message, MessageQueue mq) {
        MessageQueue opQueue;
        if (opQueueMap.containsKey(mq)) {
            opQueue = opQueueMap.get(mq);
        } else {
            opQueue = getOpQueueByHalf(mq);
            MessageQueue oldQueue = opQueueMap.putIfAbsent(mq, opQueue);
            if (oldQueue != null) {
                opQueue = oldQueue;
            }
        }
        if (opQueue == null) {
            opQueue = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), mq.getBrokerName(), mq.getQueueId());
        }
        putMessage(makeOpMessageInner(message, opQueue));
}

首先是根据prepare消息的队列逻辑位置构建了一个operation消息,即operation消息的body内容即为prepare消息的逻辑队列位置。然后同样更改其主题号,这里为RMQ_SYS_TRANS_OP_HALF_TOPIC,保存到commitlog中,也使得消费者无法消费到该消息。并且保存prepare消息队列与operation消息队列的映射关系,后面回查事务状态时会利用到该映射信息。

可能由于各种原因导致客户端发送的事务结束消息失败,所以服务端还设计了回查的功能,即定时到客户端查询未结束的prepare事务状态。

这里回查事务状态有两个参数,一个是事务过期时间,一个是最大回查次数,当事务超过最大存活时间或者回查次数超过最大次数后,都会回滚该事务。

我们继续分段来看回查逻辑:

第一步获取prepare主题对应的所有队列信息:

String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC;
Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
if (msgQueues == null || msgQueues.size() == 0) {
       log.warn("The queue of topic is empty :" + topic);
       return;
}

第二步:对所有的队列信息循环处理,根据prepare的队列信息获取operation的队列信息,并获取到两个队列的处理进度:

MessageQueue opQueue = getOpQueue(messageQueue);
long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
if (halfOffset < 0 || opOffset < 0) {
    log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue",     messageQueue,halfOffset, opOffset);
    continue;
}

第三步:从operation的处理进度处开始拉取32条消息,前面说过operation消息的body即为prepare消息的逻辑队列offset,所以如果operation的消息体里的逻辑队列offset小于prepare消息推进的最小offset,说明该消息已经处理过,保存到doneOpOffset中,后面用来更新operation的推进进度。如果大于prepare消息推进的最小offset,说明该消息已经事务结束,不需要再回查处理,保存到removeMap中,后面回查时会判断是否在存在removeMap中,存在的话,不再回查。

private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap,
        MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) {
        PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32);
        if (null == pullResult) {
            return null;
        }
        if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL
            || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) {
            log.warn("The miss op offset={} in queue={} is illegal, pullResult={}", pullOffsetOfOp, opQueue,
                pullResult);
            transactionalMessageBridge.updateConsumeOffset(opQueue, pullResult.getNextBeginOffset());
            return pullResult;
        } else if (pullResult.getPullStatus() == PullStatus.NO_NEW_MSG) {
            log.warn("The miss op offset={} in queue={} is NO_NEW_MSG, pullResult={}", pullOffsetOfOp, opQueue,
                pullResult);
            return pullResult;
        }
        List<MessageExt> opMsg = pullResult.getMsgFoundList();
        if (opMsg == null) {
            log.warn("The miss op offset={} in queue={} is empty, pullResult={}", pullOffsetOfOp, opQueue, pullResult);
            return pullResult;
        }
        for (MessageExt opMessageExt : opMsg) {
            Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset));
            log.debug("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(),
                opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffset);
            if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) {
                if (queueOffset < miniOffset) {
                    doneOpOffset.add(opMessageExt.getQueueOffset());
                } else {
                    removeMap.put(queueOffset, opMessageExt.getQueueOffset());
                }
            } else {
                log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt);
            }
        }
        log.debug("Remove map: {}", removeMap);
        log.debug("Done op list: {}", doneOpOffset);
        return pullResult;
    }

第四步:开始执行回查处理

这里逻辑较为复杂,我们再拆开来看:

 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);
}

首先是RocketMQ的一贯作风,判断定时任务执行是否超时,等待下一次执行。

然后是判断removeMap中是否包含该offset,即前面所讲,该offset对应的prepare消息事务是否已经结束,如果已经结束的话,将该offset添加到doneOpOffset中,说明该offset已经处理过,不需要再回查。

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;
    }
}

再然后是根据prepare推进的offset获取该条消息,如果结果为空,并且重试次数超过设置的拉取空最大次数,则结束该回查循环。如果没有新的消息也结束回查。如果是其他原因,则从下一个offset位置开始重新拉取消息。

if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
    listener.resolveDiscardMsg(msgExt);
    newOffset = i + 1;
    i++;
    continue;
}
if (msgExt.getStoreTimestamp() >= startTime) {
    log.debug("Fresh stored. the miss offset={}, check it later, store={}", i,
    new Date(msgExt.getStoreTimestamp()));
    break;
}

接着又是两个判断,首先判断该消息是否可以丢弃,或者跳过。丢弃的判断条件就是该消息已经回查次数是否已经超过了最大回查次数,这里每条用一次该方法,都会设置该消息的PROPERTY_TRANSACTION_CHECK_TIMES属性加1。跳过的判断依据是该消息的存储时间是否已经超过了文件的最大保留时间。

然后在判断消息的保存时间是否大于check任务执行时间,如果大于的话,说明消息都是新存储的,也退出回查。

 long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
long checkImmunityTime = transactionTimeout;
String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
if (null != checkImmunityTimeStr) {
    checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
    if (valueOfCurrentMinusBorn < checkImmunityTime) {
        if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
            newOffset = i + 1;
            i++;
            continue;
        }
    }
} else {
    if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime))         
    {
    log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
    checkImmunityTime, new Date(msgExt.getBornTimestamp()));
    break;
    }
}

接下来引入了几个变量,valueOfCurrentMinusBorn 表示已经保存的时间,用当前时间减去消息存储的时间。checkImmunityTime 表示第一次回查的最快时间,即消息从保存开始这段时间内,不需要进行消息回查。即如果消息的保存时间小于该时间,不对消息进行回查。这是因为RocketMQ认为事务需要一段时间来执行,这段时间prepare消息事务没有结束,并不代表客户端发送的事务结束消息丢失,而是客户端还处于事务执行阶段,此时进行回查属于浪费操作。

如果该消息处于非回查时间时,RocketMQ并没有结束这一次回查,而是采用了一种更为高效的方式,把该消息重新保存到prepare主题的最新位置,并推进回查的位置。同样后面消息回查前,也重复保存了prepare消息。这是因为首先简化了推进的方式,prepare消息只需要单调向前推进即可,不会出现处理offset的不连续,并且每次回查前都会对已经处理过得消息做判断,不会重复回查。其次为了提高性能,commitlog都是顺序写,如果要修改回查次数,就需要随机写。

 if (isNeedCheck) {
    if (!putBackHalfMsgQueue(msgExt, i)) {
        continue;
    }
    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;
}

接下来就是进行回查,如上所述,先把prepare消息又重新保存了一遍,然后开启一个异步任务想发送客户端进行回查事务状态。

if (newOffset != halfOffset) {
    transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
}
long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
if (newOpOffset != opOffset) {
    transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
}

最后,就是根据新的推进进度来更新prepare主题和operation主题对应的推进进度了。

以上就是RocketMQ关于事务消息的实现,还是秉承了RocketMQ一贯的替换主题和绝对顺序写的做法。利用存储的冗余换来了一定的性能提升。

并且RocketMQ客户端与服务端长连接的特点,也保证了事务消息的可靠性,即使由于网络等问题导致结束事务消息丢失,也可以由服务端主动的回查保证可靠性。并且回查进度推进这一部分的设计思想,很值得深入思考。利用了一定的数据冗余,极大简化了流程的复杂程度,也在一定程度上保证了顺序写,提高了性能。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值