• RocketMQ源码分析之RocketMQ事务消息实现原理中篇----事务消息状态回查


    摘要: 本文详细分析了RocketMQ事务消息实现原理中的事务状态回查实现,RocketMQ会默认一分钟的频率处理消息状态为Prepare的消息,通过调用消息生产者的事务状态查询接口得知消息的事务状态,从而决定提交或回滚消息。

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群

    上节已经梳理了RocketMQ发送事务消息的流程(基于二阶段提交),本节将继续深入学习事务状态消息回查,我们知道,第一次提交到消息服务器时消息的主题被替换为RMQ_SYS_TRANS_HALF_TOPIC,本地事务执行完后如果返回本地事务状态为UN_KNOW时,第二次提交到服务器时将不会做任何操作,也就是说此时消息还存在与RMQ_SYS_TRANS_HALF_TOPIC主题中,并不能被消息消费者消费,那这些消息最终如何被提交或回滚呢?

    原来RocketMQ使用TransactionalMessageCheckService线程定时去检测
    RMQ_SYS_TRANS_HALF_TOPIC主题中的消息,回查消息的事务状态。TransactionalMessageCheckService的检测频率默认1分钟,可通过在broker.conf文件中设置transactionCheckInterval的值来改变默认值,单位为毫秒。

    接下来将深入分析该线程的实现原理,从而解开事务消息回查机制。

    TransactionalMessageCheckService#onWaitEnd
    protected void onWaitEnd() {
            long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();         // @1
            int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();    // @2
            long begin = System.currentTimeMillis();
            log.info("Begin to check prepare message, begin time:{}", begin);
            this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());       // @3
            log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
        }

    代码@1:从broker配置文件中获取transactionTimeOut参数值。
    代码@2:从broker配置文件中获取transactionCheckMax参数值,表示事务的最大检测次数,如果超过检测次数,消息会默认为丢弃,即回滚消息。

    接下来重点分析TransactionalMessageService#check的实现逻辑:

    org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl
    TransactionalMessageServiceImpl#check
    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;
    }

    step1:根据主题名称,获取该主题下所有的消息队列。

    TransactionalMessageServiceImpl#check
    for (MessageQueue messageQueue : msgQueues) {
        // ...
    }
    

    Step2:循环遍历消息队列,从单个消息消费队列去获取消息。

    TransactionalMessageServiceImpl#check
    long startTime = System.currentTimeMillis();
    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;
    }

    Step3:获取对应的操作队列,其主题为:RMQ_SYS_TRANS_OP_HALF_TOPIC,然后获取操作队列的消费进度、待操作的消费队列的消费进度,如果任意一小于0,忽略该消息队列,继续处理下一个队列。

    TransactionalMessageServiceImpl#check
    List<Long> doneOpOffset = new ArrayList<>();
    HashMap<Long, Long> removeMap = new HashMap<>();
    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;
    }

    Step4:调用fillOpRemoveMap主题填充removeMap、doneOpOffset数据结构,这里主要的目的是避免重复调用事务回查接口,这里说一下RMQ_SYS_TRANS_HALF_TOPIC、RMQ_SYS_TRANS_OP_HALF_TOPIC这两个主题的作用。
    RMQ_SYS_TRANS_HALF_TOPIC:prepare消息的主题,事务消息首先先进入到该主题。
    RMQ_SYS_TRANS_OP_HALF_TOPIC:当消息服务器收到事务消息的提交或回滚请求后,会将消息存储在该主题下。

    TransactionalMessageServiceImpl#check
    // single thread
    int getMessageNullCount = 1;
    long newOffset = halfOffset;
    long i = halfOffset;                         // @1 
    while (true) {                                   
    if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {                                        // @2
              log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
            break;
          }
          if (removeMap.containsKey(i)) {         // @3
                log.info("Half offset {} has been committed/rolled back", i);
                removeMap.remove(i);
          } else {
                GetResult getResult = getHalfMsg(messageQueue, i);      // @4
                MessageExt msgExt = getResult.getMsg();   
                if (msgExt == null) {       // @5
                    if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {      
                        break;
                    }
                    if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {
                           log.info("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;
                   }
           }
    
           if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {    // @6
                    listener.resolveDiscardMsg(msgExt);
                    newOffset = i + 1;
                    i++;
                    continue;
           }
           if (msgExt.getStoreTimestamp() >= startTime) {
                   log.info("Fresh stored. the miss offset={}, check it later, store={}", i,
                                    new Date(msgExt.getStoreTimestamp()));
                   break;
           }
    
           long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();     // @7
           long checkImmunityTime = transactionTimeout;                                                                           
           String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
           if (null != checkImmunityTimeStr) {  // @8
                 checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
                 if (valueOfCurrentMinusBorn < checkImmunityTime) {
                       if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt, checkImmunityTime)) {
                              newOffset = i + 1;
                              i++;
                             continue;
                        }
                  }
            } else {   // @9
                  if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
                        log.info("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
                        checkImmunityTime, new Date(msgExt.getBornTimestamp()));
                        break;
                   }
           }
           List<MessageExt> opMsg = pullResult.getMsgFoundList();
           boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
                     || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
                     || (valueOfCurrentMinusBorn <= -1);     // @10
    
            if (isNeedCheck) {
                    if (!putBackHalfMsgQueue(msgExt, i)) {    // @11
                           continue;
                    }
                    listener.resolveHalfMsg(msgExt);
            } else {
                    pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);   // @12
                    log.info("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
                                    messageQueue, pullResult);
                    continue;
            }
       }
      newOffset = i + 1;
      i++;
    }
    if (newOffset != halfOffset) {    // @13
         transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
    }
    long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
    if (newOpOffset != opOffset) {  // @14                       
         transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
    }

    本段代码比较长,却是事务状态回查的重点实现。
    代码@1:先解释几个局部变量的含义。

    • getMessageNullCount :获取空消息的次数
    • newOffset :当前处理RMQ_SYS_TRANS_HALF_TOPIC#queueId的最新进度
    • i:当前处理消息的队列偏移量,其主题依然为RMQ_SYS_TRANS_HALF_TOPIC。

    代码@2:这段代码应该不陌生,这是RocketMQ处理任务的一个通用处理逻辑,就是一个任务处理,可以限制每次最多处理的时间,RocketMQ为待检测主题RMQ_SYS_TRANS_HALF_TOPIC的每个队列,做事务状态回查,一次最多不超过60S,目前该值不可配置。

    代码@3:如果removeMap中包含当前处理的消息,则继续下一条,removeMap中的值是通过Step3中填充的,具体实现逻辑是从RMQ_SYS_TRANS_OP_HALF_TOPIC主题中拉取32条,如果拉取的消息队列偏移量大于等于RMQ_SYS_TRANS_HALF_TOPIC#queueId当前的处理进度时,会添加到removeMap中,表示已处理过。
    代码@4:根据消息队列偏移量i从消费队列中获取消息。
    代码@5:如果消息为空,则根据允许重复次数进行操作,默认重试一次,目前不可配置。其具体实现为:

    1. 如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。
    2. 如果是由于没有新的消息而返回为空(拉取状态为:PullStatus.NO_NEW_MSG),则结束该消息队列的事务状态回查。
      1.其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取。

    代码@6:判断该消息是否需要discard(吞没,丢弃,不处理)、或skip(跳过),其依据如下:

    1. needDiscard 依据:如果该消息回查的次数超过允许的最大回查次数,则该消息将被丢弃,即事务消息提交失败,不能被消费者消费,其做法,主要是每回查一次,在消息属性TRANSACTION_CHECK_TIMES中增1,默认最大回查次数为5次。
    2. needSkip依据:如果事务消息超过文件的过期时间,默认72小时(具体请查看RocketMQ过期文件相关内容),则跳过该消息。

    代码@7:处理事务超时相关概念,先解释几个局部变量:、

    • valueOfCurrentMinusBorn :该消息已存储的时间,等于系统当前时间减去消息存储的时间戳。
    • checkImmunityTime :立即检测事务消息的时间。
    • transactionTimeout:事务消息的超时时间,其设计的意义是,应用程序在发送事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序事务提交的时间,在这段时间内,RocketMQ任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。

      代码@8:如果消息指定了事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS),如果当前时间已超过该值。

    代码@9:如果当前时间还未过(应用程序事务结束时间),则跳出本次回查处理的,等下一次再试。

    代码@10:判断是否需要发送事务回查消息,具体逻辑:

    1. 如果从操作队列(RMQ_SYS_TRANS_OP_HALF_TOPIC)中没有已处理消息并且已经超过(应用程序事务结束时间),参数transactionTimeOut值。
    2. 如果操作队列不为空,并且最后一天条消息的存储时间已经超过transactionTimeOut值。

    代码@11:如果需要发送事务状态回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC主题中,发送成功则返回true,否则返回false,这里还有一个实现关键点:

    if (putMessageResult != null
                && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
                msgExt.setQueueOffset(
                    putMessageResult.getAppendMessageResult().getLogicsOffset());
                msgExt.setCommitLogOffset(
                    putMessageResult.getAppendMessageResult().getWroteOffset());
                msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId());
    }

    如果发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量,为什么需要这样呢,答案在listener.resolveHalfMsg(msgExt)中。

    AbstractTransactionalMessageCheckListener#resolveHalfMsg
    public void resolveHalfMsg(final MessageExt msgExt) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        sendCheckMessage(msgExt);
                    } catch (Exception e) {
                        LOGGER.error("Send check message error!", e);
                    }
                }
            });
        }

    发送具体的事务回查机制,这里用一个线程池来异步发送回查消息,为了回查进度保存的简化,这里只要发送了回查消息,当前回查进度会向前推动,如果回查失败,上一步骤新增的消息将可以再次发送回查消息,那如果回查消息发送成功,那会不会下一次又重复发送回查消息呢?这个可以根据OP队列中的消息来判断是否重复,如果回查消息发送成功并且消息服务器完成提交或回滚操作,这条消息会发送到OP队列中,然后fillOpRemoveMap根据处理进度获取一批已处理的消息,来与消息判断是否重复,由于fillopRemoveMap一次只拉32条消息,那又如何保证一定能拉取到与当前消息的处理记录呢?其实就是通过代码@10来实现的,如果此批消息最后一条未超过事务延迟消息,则继续拉取更多消息进行判断(@12)和(@14),op队列也会随着回查进度的推进而推进。

    代码@12:如果无法判断是否发送回查消息,则加载更多的已处理消息进行刷选。

    代码@13:保存(Prepare)消息队列的回查进度。

    代码@14:保存处理队列(op)的进度。

    上述讲解了TransactionalMessageCheckService回查定时线程的发送回查消息的整体流程与实现细节,接下来重点分析一下上述步骤@11,通过异步方式发送消息回查的实现过程。

    AbstractTransactionalMessageCheckListener#sendCheckMessage
    public void sendCheckMessage(MessageExt msgExt) throws Exception {
            CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader();   
            checkTransactionStateRequestHeader.setCommitLogOffset(msgExt.getCommitLogOffset());
            checkTransactionStateRequestHeader.setOffsetMsgId(msgExt.getMsgId());
            checkTransactionStateRequestHeader.setMsgId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
            checkTransactionStateRequestHeader.setTransactionId(checkTransactionStateRequestHeader.getMsgId());
            checkTransactionStateRequestHeader.setTranStateTableOffset(msgExt.getQueueOffset());    // @1
            msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
            msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
            msgExt.setStoreSize(0);      // @2
            String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);     // @3
            Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId);
            if (channel != null) {
                brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt);     // @4
            } else {
                LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId);
            }
        }

    代码@1:首先构建回查事务状态请求消息,请求核心参数包括:消息offsetId、消息ID(索引)、消息事务ID、事务消息队列中的偏移量(RMQ_SYS_TRANS_HALF_TOPIC)。
    代码@2:恢复原消息的主题、队列,并设置storeSize为0。
    代码@3:获取生产者组名称。
    代码@4:根据生产者组获取任意一个生产者,通过与其连接发送事务回查消息,回查消息的请求者为【Broker服务器】,接收者为(client,具体为消息生产者)。
    其处理类为:org.apache.rocketmq.client.impl.ClientRemotingProcessor#processRequest,其详细逻辑实现方法为:

    ClientRemotingProcessor#checkTransactionState
    public RemotingCommand checkTransactionState(ChannelHandlerContext ctx,
            RemotingCommand request) throws RemotingCommandException {
            final CheckTransactionStateRequestHeader requestHeader =
                (CheckTransactionStateRequestHeader) request.decodeCommandCustomHeader(CheckTransactionStateRequestHeader.class);
            final ByteBuffer byteBuffer = ByteBuffer.wrap(request.getBody());
            final MessageExt messageExt = MessageDecoder.decode(byteBuffer);
            if (messageExt != null) {
                String transactionId = messageExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                if (null != transactionId && !"".equals(transactionId)) {
                    messageExt.setTransactionId(transactionId);
                }
                final String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
                if (group != null) {
                    MQProducerInner producer = this.mqClientFactory.selectProducer(group);
                    if (producer != null) {
                        final String addr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
                        producer.checkTransactionState(addr, messageExt, requestHeader);     // @1
                    } else {
                        log.debug("checkTransactionState, pick producer by group[{}] failed", group);
                    }
                } else {
                    log.warn("checkTransactionState, pick producer group failed");
                }
            } else {
                log.warn("checkTransactionState, decode message failed");
            }
    
            return null;
        }

    代码@1:最终调用生产者的checkTransactionState方法。

    DefaultMQProducerImpl#checkTransactionState
    public void checkTransactionState(final String addr, final MessageExt msg,
            final CheckTransactionStateRequestHeader header) {
            Runnable request = new Runnable() {        // @1
                private final String brokerAddr = addr;
                private final MessageExt message = msg;
                private final CheckTransactionStateRequestHeader checkRequestHeader = header;
                private final String group = DefaultMQProducerImpl.this.defaultMQProducer.getProducerGroup();
    
                @Override
                public void run() {
                    TransactionListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener();      // @1
                    if (transactionCheckListener != null) {
                        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
                        Throwable exception = null;
                        try {
                            localTransactionState = transactionCheckListener.checkLocalTransaction(message);            // @2
                        } catch (Throwable e) {
                            log.error("Broker call checkTransactionState, but checkLocalTransactionState exception", e);
                            exception = e;
                        }
    
                        this.processTransactionState(                                                                                                       // @3
                            localTransactionState,
                            group,
                            exception);
                    } else {
                        log.warn("checkTransactionState, pick transactionCheckListener by group[{}] failed", group);
                    }
                }
    
                private void processTransactionState(
                    final LocalTransactionState localTransactionState,
                    final String producerGroup,
                    final Throwable exception) {
                    final EndTransactionRequestHeader thisHeader = new EndTransactionRequestHeader();
                    thisHeader.setCommitLogOffset(checkRequestHeader.getCommitLogOffset());
                    thisHeader.setProducerGroup(producerGroup);
                    thisHeader.setTranStateTableOffset(checkRequestHeader.getTranStateTableOffset());
                    thisHeader.setFromTransactionCheck(true);
    
                    String uniqueKey = message.getProperties().get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (uniqueKey == null) {
                        uniqueKey = message.getMsgId();
                    }
                    thisHeader.setMsgId(uniqueKey);
                    thisHeader.setTransactionId(checkRequestHeader.getTransactionId());
                    switch (localTransactionState) {
                        case COMMIT_MESSAGE:
                            thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
                            break;
                        case ROLLBACK_MESSAGE:
                            thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
                            log.warn("when broker check, client rollback this transaction, {}", thisHeader);
                            break;
                        case UNKNOW:
                            thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
                            log.warn("when broker check, client does not know this transaction state, {}", thisHeader);
                            break;
                        default:
                            break;
                    }
    
                    String remark = null;
                    if (exception != null) {
                        remark = "checkLocalTransactionState Exception: " + RemotingHelper.exceptionSimpleDesc(exception);
                    }
    
                    try {
                        DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark,
                            3000);
                    } catch (Exception e) {
                        log.error("endTransactionOneway exception", e);
                    }
                }
            };
    
            this.checkExecutor.submit(request);    
        }

    上述代码虽多,其实实现思路非常清晰,先使用一个匿名类( Runnable )构建一个运行任务,然后提交到checkExecutor线程池中执行,这与我第一篇文章的猜测是吻合的,那重点分析一下该任务的允许逻辑,对应在run方法中。
    代码@1:获取消息发送者的TransactionListener。
    代码@2:执行TransactionListener#checkLocalTransaction,检测本地事务状态,也就是应用程序需要实现TransactionListener#checkLocalTransaction,告知RocketMQ该事务的事务状态,然后返回COMMIT_MESSAGE、ROLLBACK_MESSAGE、UNKNOW中的一个,然后向Broker发送END_TRANSACTION命令即可,
    代码@3:发送END_TRANSACTION到Broker,其具体实现,已经在 https://blog.csdn.net/prestigeding/article/details/81263833 中详细讲解过,在此不重复分析。

    到这里,事务消息状态回查流程就讲解完毕,接下来以一张流程图结束本篇的讲解。

    clipboard

    下一篇,将重点分析Broker在收到事务状态为COMMIT_MESSAGE、ROLLBACK_MESSAGE时如何提交、回滚事务。

    若您对RocketMQ技术感兴趣,请加入作者所在的 RocketMQ技术交流群

  • 相关阅读:
    html area标签 语法
    html applet标签 语法
    html address标签 语法
    html acronym标签 语法
    html abbr标签 语法
    html a标签 语法
    mysql MAX()函数 语法
    mysql LAST()函数 语法
    mysql FIRST()函数 语法
    mysql COUNT()函数 语法
  • 原文地址:https://www.cnblogs.com/zhaowei121/p/10239106.html
Copyright © 2020-2023  润新知