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...

相关推荐
我命由我123455 小时前
Java 泛型 - Java 泛型通配符(上界通配符、下界通配符、无界通配符、PECS 原则)
java·开发语言·后端·java-ee·intellij-idea·idea·intellij idea
szhf785 小时前
SpringBoot Test详解
spring boot·后端·log4j
无尽的沉默5 小时前
SpringBoot整合Redis
spring boot·redis·后端
摸鱼的春哥5 小时前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Victor3565 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack5 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo5 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3565 小时前
MongoDB(3)什么是文档(Document)?
后端
牛奔7 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌12 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp