RocketMQ 源码学习--Message-02 事务消息源码全解析(万字)

如何使用

  • 直接看官方案例

    /rocketmq-example/src/main/java/org/apache/rocketmq/example/transaction

  • 源码

    ini 复制代码
    public static void main(String[] args) throws MQClientException, InterruptedException {
      //K1 创建TransactionListener 实例,字面理解为事务消息事件监听器
      TransactionListener transactionListener = new TransactionListenerImpl();
      TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
    ​
      //K1 ExecutorService executorService,创建一个线程池,其线程的名称前缀"client-transaction-msg-check-thread",
      // 从字面理解为客户端事务消息状态检测线程,我们可以大胆的猜测一下是不是这个线程池调用TransactionListener方法,完成对事务消息的检测呢?
      // 【这里只是作者的猜测,大家不能当真,在作者后续文章发布后,如果该观点错误,会加以修复,这里写出来,主要是想分享一下我读源码的方法】
      ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
          Thread thread = new Thread(r);
          thread.setName("client-transaction-msg-check-thread");
          return thread;
        }
      });
    ​
    ​
      //K1 为事务消息发送者设置线程池。
      producer.setExecutorService(executorService);
    ​
      //K1 为事务消息发送者设置事务监听器
      producer.setTransactionListener(transactionListener);
      producer.start();
    ​
      String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
      for (int i = 0; i < 10; i++) {
        try {
          Message msg =
            new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
          SendResult sendResult = producer.sendMessageInTransaction(msg, null);
          System.out.printf("%s%n", sendResult);
    ​
          Thread.sleep(10);
        } catch (MQClientException | UnsupportedEncodingException e) {
          e.printStackTrace();
        }
      }
    ​
      for (int i = 0; i < 100000; i++) {
        Thread.sleep(1000);
      }
      producer.shutdown();
    }
  • 接下来看看监听器的源码

    java 复制代码
    public class TransactionListenerImpl implements TransactionListener {
      private AtomicInteger transactionIndex = new AtomicInteger(0);
    ​
      private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
    ​
      @Override
      public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
      }
    ​
    ​
      /**
      * 记录本地事务的事务状态
      */
      @Override
      public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
          switch (status) {
            case 0:
              return LocalTransactionState.UNKNOW;
            case 1:
              return LocalTransactionState.COMMIT_MESSAGE;
            case 2:
              return LocalTransactionState.ROLLBACK_MESSAGE;
            default:
              return LocalTransactionState.COMMIT_MESSAGE;
          }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
      }
    }

源码分析

第一次提交

生产端

  • 从上面的代码可以看出,入口在 producer.sendMessageInTransaction

DefaultMQProducerImpl

sendMessageInTransaction
  1. 基本参数校验,忽略延迟级别,是不是可以说事务消息和延迟消息不共存呢???后面看看具体Broker端的逻辑,此处暂且这么认为

    csharp 复制代码
    //K1 必要参数校验
    if (null == localTransactionExecuter && null == transactionListener) {
      throw new MQClientException("tranExecutor is null", null);
    }
    ​
    //IMP 延迟级别不生效
    // ignore DelayTimeLevel parameter
    if (msg.getDelayTimeLevel() != 0) {
      MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
    }
  1. 设置事务消息特有的标记,这个见多不怪了,不管是顺序消息,还是重试消息,都会在Properties中记录一些额外的属性信息,这个方法挺好的。还有访问的工具类,可以学习一波,用于Map中常量的设置

    kotlin 复制代码
    //IMP 事务消息的标志
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
    ​
    //IMP 设置生产者组
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
  2. 发送事务消息

    csharp 复制代码
    try {
      //K1 同步发送消息
      sendResult = this.send(msg);
    } catch (Exception e) {
      throw new MQClientException("send message Exception", e);
    }
  3. 判断发送状态,如果发送成功的话,执行本地事务方法,此处有几个疑问的地方

    1. 为啥存储了两次transactionId,而且还是不同地方获取,不同地方存储,什么情况,难道是新老版本兼容的问题
    2. 只有发送成功,还有一些超时错误,如果发送失败呢,难道发送失败就是UNKNOW吗
    ini 复制代码
    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);
          }
    ​
          //K1 执行本地事务方法
          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;
    }
  1. 根据发送状态,执行后续的逻辑处理,感觉应该有重试机制,还有本地事务执行成功,失败,对应的半消息的提交和回滚操作,暂且想想,后续细看

    ini 复制代码
    //K1 执行事务预提交之后的逻辑
    try {
      this.endTransaction(msg, 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;

    上面的send方法最后会走到通用的 sendDefaultImpl方法中,主要观察有关事务的内容

sendKernelImpl
  1. 发送消息之前,判断是否是事务消息,如果是的话,设置一个标记位,表示此刻的消息是事务消息,需要特殊处理

    ini 复制代码
    //事务消息标志,prepare消息
    final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (Boolean.parseBoolean(tranMsg)) {
      sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
    }

    最后会走到 MQClientAPIImpl 的方法中进行发送消息,此处的逻辑按照设计肯定是一致的,只是消息的一些标志位为事务消息

sendMessage
  1. 构造了事务消息的recommandCode为SEND_MESSAGE

    ini 复制代码
    request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
  2. 根据不同的方式发送消息,事务消息,在一开始发送消息的时候就设置好了SYNC,因为篇幅有限,所以简化掉了,可以去源码中查看

    arduino 复制代码
    case SYNC:
    ​
    /*
    * 发送消息之前,进行超时检查,如果已经超时了那么取消本次发送操作,抛出异常
    */
    long costTimeSync = System.currentTimeMillis() - beginStartTime;
    if (timeout < costTimeSync) {
      throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
    }
    ​
    /*
    * K2 10 发送单向、同步消息
    */
    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
      brokerAddr,
      mq.getBrokerName(),
      msg,
      requestHeader,
      timeout - costTimeSync,
      communicationMode,
      context,
      this);
    break;
  3. 各种重载之后,最后落到了下面的方法中,此刻的request与普通消息有什么不同呢

    1. request的RequestHeader中的sysFlag存在着事务消息标志位
    2. request的RequestHeader中的存在着事务消息标志位,还有生产者组信息
    java 复制代码
    private SendResult sendMessageSync(
      final String addr,
      final String brokerName,
      final Message msg,
      final long timeoutMillis,
      final RemotingCommand request
    ) throws RemotingException, MQBrokerException, InterruptedException {
      /*
      * K2 发送同步消息,request中会存储消息体信息
      */
      RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
      assert response != null;
      /*
      * K2 处理响应结果
      */
      return this.processSendResponse(brokerName, msg, response, addr);
    }
    scss 复制代码
    SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
    requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    requestHeader.setTopic(msg.getTopic());
    requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
    requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
    requestHeader.setQueueId(mq.getQueueId());
    ​
     //包含了事务消息
    requestHeader.setSysFlag(sysFlag);
    requestHeader.setBornTimestamp(System.currentTimeMillis());
    requestHeader.setFlag(msg.getFlag());
    ​
    //msg的所有属性都会给到requestHeader
    requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
    requestHeader.setReconsumeTimes(0);
    requestHeader.setUnitMode(this.isUnitMode());
    requestHeader.setBatch(msg instanceof MessageBatch);

发送端,就是和普通消息没什么区别,就是加入了标志位,recommandCode也是正常的发送消息的Code

Broker端

根据recommandCode找到处理消息下的处理类

SendMessageProcessor

asyncSendMessage
  • 获取到事务消息标记位,如果当前的Broker没有权限 的话,直接返回没有权限执行,第一种错误,等会看看这些错误Producer收到之后是怎么处理的,所有的返回用蓝色来表示
kotlin 复制代码
String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (transFlag != null && Boolean.parseBoolean(transFlag)) {
  //判断是否需要拒绝事务消息,如果需要拒绝,则返回NO_PERMISSION异常
  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);
  }
  //调用asyncPrepareMessage方法以异步的方式处理、存储事务准备消息,底层仍是asyncPutMessage方法
  putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
}

上面的 asyncPrepareMessage,不断地去重载调用,下面只针对方法,没有写出类

typescript 复制代码
@Override
public CompletableFuture<PutMessageResult> asyncPrepareMessage(MessageExtBrokerInner messageInner) {
  return transactionalMessageBridge.asyncPutHalfMessage(messageInner);
}
typescript 复制代码
public CompletableFuture<PutMessageResult> asyncPutHalfMessage(MessageExtBrokerInner messageInner) {
  return store.asyncPutMessage(parseHalfMessageInner(messageInner));
}

parseHalfMessageInner,什么意思,主要针对事务消息,进行进一步封装,啥意思呢,不知道记不记得之前学习的重试消息之类的,MQ这边充分利用的主题和队列的概念,如果队列变化,主题需要进行重置,以及Properties中隐藏真实的主题,此处也是这样,具体操作呢:备份消息的原主题名称与原队列ID,然后取消是事务消息的消息标签,重新设置消息的主题为:RMQ_SYS_TRANS_HALF_TOPIC,队列ID固定为0

less 复制代码
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
  //K1 备份原主题
  MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
​
  //K1 备份原队列ID
  MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
                              String.valueOf(msgInner.getQueueId()));
​
  //K1 取消事务标签,怎么说呢,因为这个东西就是判断失误消息的,现在没用了,而且变成了新的主题消息,那么就没用了
  msgInner.setSysFlag(
    MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
​
  //K1 构造半消息主题,常量
  msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
​
  //K1 队列为0
  msgInner.setQueueId(0);
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
  return msgInner;
}

最后调用 DefaultMessageStore 的store方法进行存储消息,现在把事务消息存储到了半消息主题中。回到上面 sendMessageInTransaction方法的第四步,针对事务消息发送之后的状态进行处理

第一次提交之后,本地处理

生产端

DefaultMQProducerImpl

sendMessageInTransaction
  • 注意:TransactionListener#executeLocalTransaction是在发送者成功发送PREPARED消息后,会执行本地事务方法,然后返回本地事务状态;如果PREPARED消息发送失败,则不会调用TransactionListener#executeLocalTransaction,并且本地事务消息,设置为LocalTransactionState.ROLLBACK_MESSAGE,表示消息需要被回滚。
ini 复制代码
//本地事务状态
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);
      }
​
      //K1 执行本地事务方法
      if (null != localTransactionExecuter) {
        
        //IMP 执行本地业务方法,需要用户自定义去实现
        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;
}
  • 根据远程消息和本地事务的执行状态,获取到执行状态,判断当前的事务消息是需要提交还是回滚
ini 复制代码
//K1 执行事务预提交之后的逻辑
try {
  this.endTransaction(msg, 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;
endTransaction
  • 根据不同的事务执行情况,构造出事务请求,结束此次事务

    • COMMIT_MESSAGE :提交事务。
    • ROLLBACK_MESSAGE:回滚事务。
    • UNKNOW:未知事务状态
    java 复制代码
    public void endTransaction(
      final Message msg,
      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());
      }
    ​
      //K2 事务ID
      String transactionId = sendResult.getTransactionId();
      final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
      EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
      requestHeader.setTransactionId(transactionId);
      requestHeader.setCommitLogOffset(id.getOffset());
    ​
      //K2 根据执行状态,设置消息的标记位
      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;
      }
    ​
      doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false);
      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());
    }

    endTransactionOneway

    • 会看到重新构造了消息体,但是此处并没有设置主题信息啥的,为啥呢???因为办消息主题就一个,而且队列也一个,所以此处只有msId,标记位之类的好吧,这也说明了,事务消息全权交给Broker进行处理,,此处的RequestCommand为 END_TRANSACTION ,此刻要注意是OneWay,啥意思,不看返回,可能会丢失这次请求
    arduino 复制代码
    public void endTransactionOneway(
      final String addr,
      final EndTransactionRequestHeader requestHeader,
      final String remark,
      final long timeoutMillis
    ) throws RemotingException, MQBrokerException, InterruptedException {
      RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
    ​
      request.setRemark(remark);
      this.remotingClient.invokeOneway(addr, request, timeoutMillis);
    }

第二次提交

根据requestCommand,可以找到处理该请求的入口在 EndTransactionProcessor

Broker端

EndTransactionProcessor

processRequest
  • 核心实现就是根据commitlogOffset找到消息,如果是提交动作,就恢复原消息的主题与队列,再次存入commitlog文件进而转到消息消费队列,供消费者消费,然后将原预处理消息存入一个新的主题RMQ_SYS_TRANS_OP_HALF_TOPIC,代表该消息已被处理;回滚消息与提交事务消息不同的是,提交事务消息会将消息恢复原主题与队列,再次存储在commitlog文件中。回滚消息,不会恢复原主题,放入到 commitLog 中
  • 针对,事务消息的 提交 和 回滚进行了处理,但是针对未知状态呢,不进行任何操作,了解过原理的,应该知道事务消息还有回查机制,那么这种回查机制如何去执行呢,后续分析,现在看看,当提交和回滚的时候,Broker进行了什么操作

    • 成功:转换成真正的消息,然后提交到真正的主题下面,删除半消息
    • 回滚:通过消息偏移量获取到消息,然后删除半消息
    • 未知:没有处理
    scss 复制代码
    //K1 提交的操作
    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) {
    ​
          //K2 获取真正的消息
          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);
    ​
          //K2 执行落盘操作
          RemotingCommand sendResult = sendFinalMessage(msgInner);
    ​
          //K2 执行成功了删除办消息
          if (sendResult.getCode() == ResponseCode.SUCCESS) {
            this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
          }
          return sendResult;
        }
        return res;
      }
    ​
      //K1 回滚的操作
    } 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);
    ​
        //K2 删除半消息
        if (res.getCode() == ResponseCode.SUCCESS) {
          this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
        }
        return res;
      }
    }
    response.setCode(result.getResponseCode());
    response.setRemark(result.getResponseRemark());
    return response;

回查机制

  • 因为commit 和 rollback 的消息是 oneway,存在丢失风险,同时,在处理提交请求的时候,并没有对 UNKWON 进行处理,那么肯定需要一个机制,确保可以准确的获取到 Producer 中本地事务的执行情况

Broker端

在 Broker 端,启动了一个事务消息的检测服务 TransactionalMessageCheckService,该服务在 Broker 启动的时候启动,默认是 1 分钟,执行一次

TransactionalMessageCheckService

  • 此类也是一个 ServiceThread,是一个线程对象
csharp 复制代码
@Override
public void run() {
  log.info("Start transaction check service thread!");
  //IMP 检测间隔 1 分钟
  long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
  while (!this.isStopped()) {
    this.waitForRunning(checkInterval);
  }
  log.info("End transaction check service thread!");
}
onWaitEnd
  • 两个重要的参数

    • TransactionTimeOut:事务的过期时间,一个消息的存储时间 + 该值 > 系统当前时间,表示事务消息还在有效期内,可以对该消息执行事务状态会查。
    • TransactionCheckMax:事务的最大检测次数,如果超过检测次数,消息会默认为丢弃,即rollback消息。
arduino 复制代码
@Override
protected void onWaitEnd() {
  //K1 事务消息的有效时间
  long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
​
  //K1 事务的最大检测次数
  int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
  long begin = System.currentTimeMillis();
  log.info("Begin to check prepare message, begin time:{}", begin);
​
  this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
  log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
}

TransactionalMessageServiceImpl

check
  • 此方法是回查方法,由于代码太长,分开来考虑
  1. 根据半消息主题,查询消息队列,由于事务消息的主题是特定的,此处也可以看出来,不可以随意改动

    ini 复制代码
    String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
    ​
    //K1  根据主题名称,获取该主题下所有的消息队列
    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);
  2. 循环遍历消息队列,从单个消息消费队列去获取消息。

    1. 此处的当前时间,非常关键,可是后面用于终止循环的逻辑
    scss 复制代码
    //K1 循环遍历消息队列,从单个消息消费队列去获取消息
    for (MessageQueue messageQueue : msgQueues) {
      
        long startTime = System.currentTimeMillis();
    ​
        //..... 下面将针对消费队列的逻辑
    }
  3. 获取两个特定主题的消费进度偏移量,用于嘛,肯定是用于获取消息用的,但是这两个主题干嘛用的呢,此处特别重要。既然是回查,那么肯定需要知道哪些消息需要去回查,还有就是回查的时候,如果避免重复回查呢,就是幂等操作呢,所以此处的两个主题为

    1. RMQ_SYS_TRANS_OP_HALF_TOPIC:此主题主要是为了在 commit 或者 rollback 的时候,会把消息加入到此主题中,可想而知,这个主题中可以获取到,哪些消息执行完成了(包括成功和回滚)
    2. RMQ_SYS_TRANS_HALF_TOPIC:既然要回查,肯定需要半消息主题了,确定一下哪些消息需要回查

    如果消费进度不合理,直接下一个消息队列进行消费即可

    ini 复制代码
    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;
    }
  4. 拉取操作主题【RMQ_SYS_TRANS_OP_HALF_TOPIC】中的 32 条消息,干嘛用的呢,想想上面说的,这个里面存储的执行过的消息,那么肯定是用于避免重复回查用的,不过这个地方,用两个结构记录了 已经执行完成过的事务消息,以当前的拉取进度为分界线。这两个接口可都是用于记录执行完成的事务消息

    1. doneOpOffset:用于记录在拉取半消息消费进度之前已经执行完成的消息

    2. removeMap:用于记录在拉取进度之后执行完成的消息。

      后续肯定会发现 随着消费进度的增加,donOpOffset 越来越多, 而 removeMap 越来越少

    ini 复制代码
    //IMP  从字面上看,是保存已经回查过的偏移量,为了避免重复调用回查接口
    //    -------doneOpOffset-------|halfOffset|--------removeMap-------
    //   doneOpOffset 表示,当前进度之前,已经处理过的消息进度
    //   removeMap 表示,对于当前进度而言,是之后的偏移量,但是已经处理过的消息偏移量
    List<Long> doneOpOffset = new ArrayList<>();
    ​
    //半消息偏移量 --》 OP 偏移量
    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;
    }
    • 调用【fillOpRemoveMap】主题填充removeMap、doneOpOffset数据结构,这里主要的目的是避免重复调用事务回查接口,具体的分割线,可以看下面源码
    kotlin 复制代码
    private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap,
                                       MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) {
    ​
      //K1 拉取 OP 主题的 32 条消息,从拉取的偏移量之后的 32 条  OP消息
      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;
      }
    ​
    ​
      //OP 中的消息,表示已经 提交火滚的消息,就是已经处理过了的消息,无需再次操作
      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);
    ​
        //k2 commit 成功或者 rollback 成功的消息
        if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) {
          if (queueOffset < miniOffset) {
            //IMP  过去式中已经处理过的消息
            doneOpOffset.add(opMessageExt.getQueueOffset());
          } else {
            //IMP 已经处理过了,但是消息队列认为没有处理的消息
            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;
    }
  5. 定义几个私有变量,用于后面进行偏移量的更新操作,这样不会改变原始的变量的值,可以进行比较等操作

    ini 复制代码
    //IMP 获取空消息的次数
    int getMessageNullCount = 1;
    ​
    //IMP 当前处理 事务半消息主题  【RMQ_SYS_TRANS_HALF_TOPIC#queueId】  的最新进度
    long newOffset = halfOffset;
    ​
    //IMP 当前处理消息的队列偏移量,其主题依然为RMQ_SYS_TRANS_HALF_TOPIC
    long i = halfOffset;
  1. 针对消息队列进行,自旋操作,还记得之前的那个开始时间吗,所以,在自旋的过程中,如果超时了,则放弃当前消息队列的执行,那么很显然,每隔消息队列的自旋时间有上限,很好的避免了不断的死循环逻辑

    c 复制代码
    while (true) {
    ​
      //K2 做事务状态回查,一次最多不超过60S,目前该值不可配置
      if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
        log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
        break;
      }
      
      //....
      
    }
  2. 如果当前的半小时偏移量,存在于 removeMap 中,也就是已经执行成功了,那么只需要更新拉取偏移量就好了,然后自旋执行下一次拉取的逻辑。如果不在呢,可能有很多中可能

    1. 消息真的没有执行
    2. 消息执行了,只是你在获取 【RMQ_SYS_TRANS_OP_HALF_TOPIC】 判断是否执行的时候,消息还没有加入到此主题中,那么就是 else 的逻辑,也是此处,最复杂的逻辑,累了可以休息一波,确实好多内容
    csharp 复制代码
    //K2 如果已经处理过了,就直接跳过,执行下一条消息如果removeMap中包含当前处理的消息,则继续下一条
    if (removeMap.containsKey(i)) {
      log.debug("Half offset {} has been committed/rolled back", i);
      Long removedOpOffset = removeMap.remove(i);
      doneOpOffset.add(removedOpOffset);
    }else{
      //....
    }

累了休息会儿,此处打个分割符,方便后面回来继续查看


回查负责逻辑
  1. 先从半消息主题队列中获取消息

    ini 复制代码
    //K2 根据消息队列偏移量i从消费队列中获取消息
    GetResult getResult = getHalfMsg(messageQueue, i);
    MessageExt msgExt = getResult.getMsg();
  2. 下面情况则执行下一个半消息队列的回查操作

    1. 没有数据,而且 获取空消息的次数操作最大次数,此处为 1
    2. 如果拉取到的消息状态,给定是没有新的消息,那么也执行下一个队列的操作
    3. 其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取
    arduino 复制代码
    //K2 如果消息为空,则根据允许重复次数进行操作,默认重试一次
    if (msgExt == null) {
      //MAX_RETRY_COUNT_WHEN_HALF_NULL = 1
      //k2 如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。
      if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {
    ​
        //IMP  跳出当前队列的处理,继续下一个消息队列
        break;
      }
    ​
      //K2 如果是由于没有新的消息而返回为空(拉取状态为:PullStatus.NO_NEW_MSG),则结束该消息队列的事务状态回查
      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());
        //IMP  跳出当前队列的处理,继续下一个消息队列
        break;
      } else {
    ​
        //k2 其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取
        log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}",
                 i, messageQueue, getMessageNullCount, getResult.getPullResult());
        i = getResult.getPullResult().getNextBeginOffset();
        newOffset = i;
        continue;
      }
    }
  3. 如果拉取到消息呢,肯定需要判断消息是否需要回查,或者是否需要跳过,此处会涉及到两个方法 【needDiscard】和 【needSkip】,此处注意一个参数,transactionCheckMax,是设置的最多的事务回查次数,如果太多次没有结果,我就放弃了,确实如此哈

    1. needDiscard:如果该消息回查的次数超过允许的最大回查次数,则该消息将被丢弃,即事务消息提交失败,不能被消费者消费,其做法,主要是每回查一次,在消息属性TRANSACTION_CHECK_TIMES中增1,默认最大回查次数为5次
    2. needSkip依据:如果事务消息超过文件的过期时间,默认72小时(具体请查看RocketMQ过期文件相关内容),则跳过该消息
    less 复制代码
    //K2 判断该消息是否需要discard(吞没,丢弃,不处理)、或skip(跳过)
    //   transactionCheckMax 最大的拉取次数
    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;
     }

    接下来是这里面最不好理解的地方,如果不清醒了,休息会再看

  1. 先了解几个变量

    1. valueOfCurrentMinusBorn:当前时间 - 消息产生的时间,那么就是半消息存活的时间了,为啥要有这个东西呢,考虑一个点,如果半消息刚落到半消息主题上,你就立刻去回查,大概率是没有结果的,所以此值, 用于避免无意义的回查操作

    2. checkImmunityTime:既然为了避免无意义的回查,那么多久之后认为是有意义的呢,好吧,这个值就是那个意义值。如果小于这个值,则认为太快查询了,停止查询,等待下次查询吧

      1. checkImmunityTimeStr: 这个值呢,就是用户设置的上面的太长时间的数值表示,表示超了这个时间呢,就立刻回查,此值是配置信息,如果设置了,就更新 checkImmunityTime 的值
    3. transactionTimeout:事务消息的超时时间,这个时间是从OP拉取的消息的最后一条消息的存储时间与check方法开始的时间,如果时间差超过了transactionTimeout,就算时间小于checkImmunityTime时间,也发送事务回查指令

    ini 复制代码
    //IMP 该消息已存储的时间,等于系统当前时间减去消息存储的时间戳
    long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
    ​
    //IMP 立即检测事务消息的时间,延迟时间,如果时间太短,还没有来得及提交或者回滚,就去查询了
    //  ,其设计的意义是,应用程序在发送事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序事务提交的时间
    //  ,在这段时间内,RocketMQ任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。
    long checkImmunityTime = transactionTimeout;
    ​
    //事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS)
    String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
  2. 如果当前时间还未过(应用程序事务结束时间),则跳出本次回查处理的,等下一次再试

    1. valueOfCurrentMinusBorn < checkImmunityTime,土话说,你太急了,再等等,对方可能在回复中
    scss 复制代码
    if (null != checkImmunityTimeStr) {
      checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
      if (valueOfCurrentMinusBorn < checkImmunityTime) {
        if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
          newOffset = i + 1;
          i++;
          continue;
        }
      }
    } else {
    ​
      //IMP 小于规定的最小检测时间,说明时间太短,此次不用回查,每隔事务都有一个执行周期,
      //    周期内,说明还没有执行完,无需进行回查,避免了无效的回查操作
      if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
        log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
                  checkImmunityTime, new Date(msgExt.getBornTimestamp()));
        break;
      }
    }
  3. 走到这里呢,一定是 valueOfCurrentMinusBorn > checkImmunityTime 肯定是当前消息的存活时间已经超了 checkImmunityTime 的时间点,但是 MQ 系统仍然有个时间点 transactionTimeout, 一种情况,如果设置的太小,岂不是还是不断的去回查消息,所以可以理解为另一种保障。下面就是判断是否需要回查的逻辑 ,直接上代码

    scss 复制代码
    //  说明有消息已经 commit  或者 rollback 了
    List<MessageExt> opMsg = pullResult.getMsgFoundList();
    boolean isNeedCheck =
      //IMP 情况一:很久没有消息反馈的,必须回查
      //如果从操作队列(RMQ_SYS_TRANS_OP_HALF_TOPIC)中没有已处理消息
      //  并且消息已经执行很久了(应用程序事务结束时间),参数transactionTimeOut值。
      //  那么次消息肯定要回查
      (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
    ​
      //如果操作队列不为空,并且最后一条消息的存储时间已经超过transactionTimeOut值。
      //IMP 情况二:已经有处理完的消息,而且当前消息不在处理消息的范围内,如果最后一条消息已经超时了,那么当前消息肯定也超时了,必须回查
      || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
      || (valueOfCurrentMinusBorn <= -1);
    • 拉取的 32 条已经执行完成的事务消息

    • 下面两种情况需要回查

      1. 操作队列为空,而且很久没有写入消息了,那么必须回查
      2. 操作队列不为空,而且最后一条消息也已经超了 回查的超时时间了,那么没有当前消息的我,是不是应该回查一下我这条消息的消费情况 了
  4. 如果不满足回查条件,怎么办呢,有可能是我拉取的时候没有,可能现在就有了,那么我继续下一个偏移量,跟新我的已经处理好的事务消息的结果,然后继续去判断上面的逻辑,这也就是为什么要加个时间限制,否则一直一个队列不断判断回查了,不就是死循环了吗

    ini 复制代码
    //K1 如果无法判断是否发送回查消息,则加载更多的已处理消息进行刷选,如果执行时间太长,就等待再次拉取之后再来判断
    //    否则就会出现重复拉取的情况了
    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;
  5. 如果满足回查条件呢,执行 l 下面的代码,有个疑问你发就发吧,还重新 put 了一遍是啥意思

    kotlin 复制代码
    ​
    //IMP 如果需要发送事务状态回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC主题中,发送成功则返回true,否则返回false
    if (!putBackHalfMsgQueue(msgExt, i)) {
      continue;
    }
    ​
    ​
    //K1 异步线程发送回查消息
    listener.resolveHalfMsg(msgExt);

    putBackHalfMsgQueue

    less 复制代码
    private boolean putBackHalfMsgQueue(MessageExt msgExt, long offset) {
    ​
      //K1 重新将当前消息加入到半消息中
      PutMessageResult putMessageResult = putBackToHalfQueueReturnResult(msgExt);
    ​
      //K1 如果发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量
      if (putMessageResult != null
          && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
    ​
        //K2  发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量
        msgExt.setQueueOffset(
          putMessageResult.getAppendMessageResult().getLogicsOffset());
        msgExt.setCommitLogOffset(
          putMessageResult.getAppendMessageResult().getWroteOffset());
        msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId());
        log.debug(
          "Send check message, the offset={} restored in queueOffset={} "
          + "commitLogOffset={} "
          + "newMsgId={} realMsgId={} topic={}",
          offset, msgExt.getQueueOffset(), msgExt.getCommitLogOffset(), msgExt.getMsgId(),
          msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX),
          msgExt.getTopic());
        return true;
      } else {
        log.error(
          "PutBackToHalfQueueReturnResult write failed, topic: {}, queueId: {}, "
          + "msgId: {}",
          msgExt.getTopic(), msgExt.getQueueId(), msgExt.getMsgId());
        return false;
      }
    }
    • 通过上面代码,为啥要有这个逻辑呀,想一个点,既然回查肯定不能阻塞回查的执行,

      • 那么必然有个情况,如果消息回查异常了怎么办,如果来重复回查呢,所以,此处将消息重新加入到了 半消息队列中。有没有一个可能,我回查成功了,但是呢现在又加入了一遍,会不会重复回查呢,当然不会了,忘了我们 removeMap 了吗,就是为了避免重复回查的。

      • 但是现在又有一个问题,removeMap 可是我拉取 32 条数据增加的消息过滤 map,怎么能保障一定能拉取到处理记录呢,也就是存在于 OP主题中呢???

        • 是否记得上面的一个逻辑【(opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout)】,如果发现回查的速度太快,拉取的时候,只是拉取了部分的消息处理数据,那么需要继续去拉取,只有等超了 事务回查超时时间,才认为是确实需要回查,所以此处消息回查

          • 如果确实超了时间,那么肯定去回查
          • 如果没有超了时间,就去拉取递增的拉取处理结果,知道拉取到所有超时之前的所有处理结果

    resolveHalfMsg

    最后向 Producer 发送了回查的 request ,requestCode 为【CHECK_TRANSACTION_STATE

    java 复制代码
    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);
          }
        }
      });
    }
    ​
    ​
    ​
    ​
    ​
    ​
    public void sendCheckMessage(MessageExt msgExt) throws Exception {
    ​
      //K1 首先构建回查事务状态请求消息,请求核心参数包括:消息offsetId、消息ID(索引)、消息事务ID、事务消息队列中的偏移量(RMQ_SYS_TRANS_HALF_TOPIC)
      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());
      msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
      msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
    ​
      //K1 恢复原消息的主题、队列,并设置storeSize为0
      msgExt.setStoreSize(0);
    ​
      //K1 获取生产者组名称
      String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
    ​
      //K1 根据生产者组获取任意一个生产者,通过与其连接发送事务回查消息,回查消息的请求者为【Broker服务器】,接收者为(client,具体为消息生产者)
      Channel channel = brokerController.getProducerManager().getAvailableChannel(groupId);
      if (channel != null) {
        brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt);
      } else {
        LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId);
      }
    }
    ​
    ​
    ​
    public void checkProducerTransactionState(
      final String group,
      final Channel channel,
      final CheckTransactionStateRequestHeader requestHeader,
      final MessageExt messageExt) throws Exception {
      RemotingCommand request =
        RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader);
      request.setBody(MessageDecoder.encode(messageExt, false));
      try {
        this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
      } catch (Exception e) {
        log.error("Check transaction failed because invoke producer exception. group={}, msgId={}, error={}",
                  group, messageExt.getMsgId(), e.toString());
      }
    }
  1. 跟新进度信息,半消息进度,和处理结果队列消费进度,这样就可以一直执行下去

    ini 复制代码
    if (newOffset != halfOffset) {
      //K1 保存(Prepare)消息队列的回查进度
      transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
    }
    long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
    if (newOpOffset != opOffset) {
      //K1 保存处理队列(op)的进度
      transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
    }

引用借鉴学习:blog.csdn.net/prestigedin...blog.csdn.net/prestigedin...

相关推荐
stevewongbuaa33 分钟前
一些烦人的go设置 goland
开发语言·后端·golang
花心蝴蝶.4 小时前
Spring MVC 综合案例
java·后端·spring
落霞的思绪4 小时前
Redis实战(黑马点评)——关于缓存(缓存更新策略、缓存穿透、缓存雪崩、缓存击穿、Redis工具)
数据库·spring boot·redis·后端·缓存
m0_748255654 小时前
环境安装与配置:全面了解 Go 语言的安装与设置
开发语言·后端·golang
SomeB1oody9 小时前
【Rust自学】14.6. 安装二进制crate
开发语言·后端·rust
患得患失94911 小时前
【Django DRF Apps】【文件上传】【断点上传】从零搭建一个普通文件上传,断点续传的App应用
数据库·后端·django·sqlite·大文件上传·断点上传
customer0812 小时前
【开源免费】基于SpringBoot+Vue.JS校园失物招领系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
中國移动丶移不动13 小时前
Java 反射与动态代理:实践中的应用与陷阱
java·spring boot·后端·spring·mybatis·hibernate
uzong14 小时前
Mybatis-plus 更新 Null 的策略踩坑记
java·后端
uzong14 小时前
mapStruct 使用踩坑指南
java·后端