RocketMQ 源码学习--Consumer-04 顺序消息消费源码解析

前提点

  • 通过前面的消息消费流程可知

    1. 重平衡,产生 PullRequest拉取请求,交给 PullMessageService 中的内存队列
    2. 消息拉取:PullMessageService 阻塞等待自旋不断去获取 PullRequest 进行消息拉取,一个消息队列对应一个拉取请求,拉取请求不断的重复塞入到队列中,实现了不断拉取的功能
    3. 消息消费:ConsumeMessageService 中 使用线程池消费 拉取到的消息

思考

感觉需要处理的两个锁

  1. 一个消费队列顺序消费的化,同一时间只能让一个消费者组里面的一个消费者消费,此时应该就有并发问题了,所以通过锁机制来进行解决,而且必须由统一的锁的地方,这样同一消费者组内的消费者才能感知。保存锁的地方,应该是 Broker 为啥不是 NameServer

    1. NameServer 可能具有延迟性,因为是各个节点不通信,如果数据不一致的化,那我是按照哪个节点的锁状态来判断呢
    2. Broker 中的消息队列,如果是同一个队列,那么 Broker 肯定是一定的,这样对于改消息队列,竞争锁的消费者就可以同一时刻判断是否有消费者获取锁
    3. 结论:消息队列,对于同一个消费者组,的锁一定是在 Broker 端竞争
  2. ConsumeMessageService消费的时候,是使用的线程池处理,好比一个场景,一开始拉取了顺序消息的消息 1,此时拉取到了消息 2 ,那次是拉取是异步的,拉取完就继续去拉,那么可能存在,消费消息 1 的时候,消息 2 也开始了消费,那岂不是就 消息 2 优先于消息 1 消费了吗,不符合顺序消息的概念了

  3. 对于普通消息而言,如果消费失败了,会加入到 RETRY 消费队列中,最后会到 SCHEDULE_XXX 队列中,这样不会影响消息队列中后续消息的执行,那么顺序消息如果保证不会因为消息 1 消费失败,造成后续消息的阻塞消费呢???观察偏移量的地方是如何处理的

针对上面的问题,看看是如何加锁的,如何进行顺序消息的消费的,如何消费的呢

分析

RebalanceImp

updateProcessQueueTableInRebalance

  • 此方法简单说就是重分配之后,构造消费对列,如果有变化或者不存在就重新构建 PullRequest 进行后续拉取操作,源码可以看到如果是顺序消息,会进行 this.lock(mq)的操作,按照我们猜想应该是在 Broker 端锁住此队列
ini 复制代码
for (MessageQueue mq : mqSet) {
​
  //K2 只处理之前不存在的
  // 消息队列--消费进度,不包含的话,属于全新的消费进度,需要重新消费
  if (!this.processQueueTable.containsKey(mq)) {
​
​
    //IMP 顺序消息,且要 锁住 消息队列
    if (isOrder && !this.lock(mq)) {
      log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
      continue;
    }
​
    this.removeDirtyOffset(mq);
    ProcessQueue pq = new ProcessQueue();
​
    long nextOffset = -1L;
    try {
      nextOffset = this.computePullFromWhereWithException(mq);
    } catch (Exception e) {
      log.info("doRebalance, {}, compute offset failed, {}", consumerGroup, mq);
      continue;
    }
​
    if (nextOffset >= 0) {
      ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
      if (pre != null) {
        log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
      } else {
        log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
        PullRequest pullRequest = new PullRequest();
        pullRequest.setConsumerGroup(consumerGroup);
        pullRequest.setNextOffset(nextOffset);
        pullRequest.setMessageQueue(mq);
        pullRequest.setProcessQueue(pq);
        pullRequestList.add(pullRequest);
        changed = true;
      }
    } else {
      log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
    }
  }
}
​
//增加拉取请求
this.dispatchPullRequest(pullRequestList);
this.lock
kotlin 复制代码
public boolean lock(final MessageQueue mq) {
​
  //K1 通过 BrokerName 获取到地址信息
  FindBrokerResult findBrokerResult = this.mQClientFactory.
    findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
  if (findBrokerResult != null) {
    LockBatchRequestBody requestBody = new LockBatchRequestBody();
    requestBody.setConsumerGroup(this.consumerGroup);
    requestBody.setClientId(this.mQClientFactory.getClientId());
    requestBody.getMqSet().add(mq);
​
    try {
​
      //K1 使用 ConsumerGroup+ClientId 一个消费者组中的消费者 力度,锁住此队列
      Set<MessageQueue> lockedMq =
        this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
​
      //K1 锁住之后,修改本地内存队列中的消费队列为锁定成功
      for (MessageQueue mmqq : lockedMq) {
        ProcessQueue processQueue = this.processQueueTable.get(mmqq);
        if (processQueue != null) {
          processQueue.setLocked(true);
          processQueue.setLastLockTimestamp(System.currentTimeMillis());
        }
      }
​
      boolean lockOK = lockedMq.contains(mq);
      log.info("the message queue lock {}, {} {}",
               lockOK ? "OK" : "Failed",
               this.consumerGroup,
               mq);
      return lockOK;
    } catch (Exception e) {
      log.error("lockBatchMQ exception, " + mq, e);
    }
  }
​
  return false;
}
  • 上面流程可以说是

    • 顺序消息时,添加该消息队列的拉取任务之前,首先要先尝试锁定消费者(消费组+CID),不同消费组的消费者可以同时锁定同一个消息消费队列,集群模式下同一个消费组内只能被一个消费者锁定,
    • 如果锁定成功,则添加到拉取任务中,如果锁定未成功,说明虽然发送了消息队列重新负载,但该消息队列还未被释放,本次负载周期不会进行消息拉取
  • 增加拉取请求之后,由 PullMessageService 进行消息拉取,下面看 PullMessageService 中涉及顺序消息的内容

PullMessageService

pullMessage

  • 如果队列,

    • 未锁定,则延迟3s后再拉取。
    • 锁定,则会设置延迟的偏移量,然后进行拉取消息
ini 复制代码
//K1 顺序消息
// 消息队列,需要锁定
if (processQueue.isLocked()) {
  if (!pullRequest.isPreviouslyLocked()) {
    long offset = -1L;
    try {
      offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());
    } catch (Exception e) {
      this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
      log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e);
      return;
    }
    boolean brokerBusy = offset < pullRequest.getNextOffset();
    log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
             pullRequest, offset, brokerBusy);
    if (brokerBusy) {
      log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
               pullRequest, offset);
    }
​
    //IMP 构造顺序消息的拉取请求,先设置先前被锁定,那么这个地方什么时候设置为false呢
    pullRequest.setPreviouslyLocked(true);
    pullRequest.setNextOffset(offset);
  }
} else {
  //K2 如果没有锁定,延迟3s拉取
  this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
  log.info("pull message later because not locked in broker, {}", pullRequest);
  return;
}
  • 同时,在拉取消息的时候,会设置回调函数,回调函数中,会存在ConsumerMessageService,用于处理拉取到的消息

    kotlin 复制代码
    DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                        pullResult.getMsgFoundList(),
                                        processQueue,
                                        pullRequest.getMessageQueue(),
                                        dispatchToConsume);

ConsumeMessageService

  • 顺序消息的,实现类为ConsumeMessageOrderlyService

submitConsumeRequest

  • 通过下面的方法看出,msgs并没有进行处理,很显然不是这样的,返回去找找,你会发现把拉取到的消息,放到了ProcessQueue的MsgTreeMap中,还有没有印象,里面包含了消息内容,tree的第一个节点是便宜量最小的消息
java 复制代码
public void submitConsumeRequest(
  final List<MessageExt> msgs,
  final ProcessQueue processQueue,
  final MessageQueue messageQueue,
  final boolean dispathToConsume) {
​
  //如果允许分发消费
  if (dispathToConsume) {
    //构建消费请求,没有将消费放进去,消费消费会自动拉取treemap中的消息
    ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
    //将请求提交到consumeExecutor线程池中进行消费
    this.consumeExecutor.submit(consumeRequest);
  }
}

ConsumeMessageOrderlyService#ConsumeRequest

基本属性

kotlin 复制代码
class ConsumeRequest implements Runnable {
  //消息处理队列。
  private final ProcessQueue processQueue;
​
  //消息队列
  private final MessageQueue messageQueue;
​
  public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) {
    this.processQueue = processQueue;
    this.messageQueue = messageQueue;
  }
​
  //.......
}

关键方法

run
  • 里面肯定是消费消息的逻辑,咋一看下面的,有点长呀,后面慢慢分析
kotlin 复制代码
public void run() {
  if (this.processQueue.isDropped()) {
    log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
    return;
  }
​
  /*
  * K2 1 消费消息之前先获取当前messageQueue的本地锁,防止并发
  * 这将导致ConsumeMessageOrderlyService的线程池中的线程将不会同时并发的消费同一个队列
  */
  final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
​
  /*
  * 阻塞式的获取同步锁,锁对象是一个Object对象,采用原生的synchronized锁定
  */
  synchronized (objLock) {
​
    /*
    * K2 2 如果是广播模式,或者是 (集群模式,并且锁定了processQueue处理队列,并且processQueue处理队列锁没有过期),那么可以消费消息
    *  processQueue处理队列锁定实际上就是在负载均衡的时候向broker申请的消息队列分布式锁,申请成功之后将processQueue.locked属性置为true
    *
    *  当前消费者通过RebalanceImpl#rebalanceByTopic分配了新的消息队列之后,对于集群模式的顺序消费会尝试通过RebalanceImpl#lock
    *  方法请求broker获取该队列的分布式锁
    *
    *  同理在ConsumeMessageOrderlyService启动的时候,其对于集群模式则会启动一个定时任务,默认每隔20s调用RebalanceImpl#lockAll方法,
    *  请求broker获取所有分配的队列的分布式锁
    */
    if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
        || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
​
      //消费起始时间
      final long beginTime = System.currentTimeMillis();
​
      /*
      * K2 3 循环继续消费,直到超时或者条件不满足退出循环
      */
      for (boolean continueConsume = true; continueConsume; ) {
        //3.1 如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费
        if (this.processQueue.isDropped()) {
          log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
          break;
        }
​
        //3.2 如果是集群模式,并且没有锁定了processQueue处理队列
        if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
            && !this.processQueue.isLocked()) {
          log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
          //对该队列请求broker获取该队列的分布式锁,然后延迟提交消费请求
          ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
          break;
        }
​
        //3.3 如果是集群模式,并且processQueue处理队列锁已经过期
        //客户端对于从broker获取的mq锁,过期时间默认30s,可以通过-Drocketmq.client.rebalance.lockMaxLiveTime参数设置
        if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
            && this.processQueue.isLockExpired()) {
          log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
          //对该队列请求broker获取该队列的分布式锁,然后延迟提交消费请求
          ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
          //结束循环,本次消费任务结束
          break;
        }
​
        //计算消费时间
        long interval = System.currentTimeMillis() - beginTime;
        //3.4 如果单次消费任务的消费时间大于默认60s,可以通过-Drocketmq.client.maxTimeConsumeContinuously配置启动参数来设置时间
        if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
          //延迟提交新的消费请求
          ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
          //结束循环,本次消费任务结束
          break;
        }
​
        //获取单次批量消费的数量,默认1,可以通过DefaultMQPushConsumer.consumeMessageBatchMaxSize的属性配置
        final int consumeBatchSize =
          ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
​
        /*
        * 3.5 从processQueue内部的msgTreeMap有序map集合中获取offset最小的consumeBatchSize条消息,
        *    按顺序从最小的offset返回,保证有序性
        */
        List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);
        //重置重试topic,当消息是重试消息的时候,将msg的topic属性从重试topic还原为真实的topic。
        defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
​
        /*
        * K2 4 如果拉取到了消息,那么进行消费
        */
        if (!msgs.isEmpty()) {
          //顺序消费上下文
          final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
​
          //消费状态
          ConsumeOrderlyStatus status = null;
​
          ConsumeMessageContext consumeMessageContext = null;
​
          /*
          * 4.1 如果有钩子,那么执行consumeMessageBefore前置方法
          */
          if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
            consumeMessageContext = new ConsumeMessageContext();
            consumeMessageContext
              .setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup());
            consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
            consumeMessageContext.setMq(messageQueue);
            consumeMessageContext.setMsgList(msgs);
            consumeMessageContext.setSuccess(false);
            // init the consume context type
            consumeMessageContext.setProps(new HashMap<String, String>());
            ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
          }
​
          //起始时间
          long beginTimestamp = System.currentTimeMillis();
          //消费返回类型
          ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
          boolean hasException = false;
          try {
            /*
            * 4.2 真正消费消息之前再获取processQueue的本地消费锁,保证消息消费时,一个处理队列不会被并发消费
            * 从这里可知,顺序消费需要获取三把锁,
            *    broker的messageQueue锁,
            *    本地的messageQueue锁,
            *    本地的processQueue锁
            */
            this.processQueue.getConsumeLock().lock();
            //如果处理队列被丢弃,那么直接返回,不再消费
            if (this.processQueue.isDropped()) {
              log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
                       this.messageQueue);
              //结束循环,本次消费任务结束
              break;
            }
​
            /*
            * 4.3 调用listener#consumeMessage方法,进行消息消费,调用实际的业务逻辑,返回执行状态结果
            * 有四种状态,ConsumeOrderlyStatus.SUCCESS 和 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT推荐使用
            * ConsumeOrderlyStatus.ROLLBACK和ConsumeOrderlyStatus.COMMIT已被废弃
            */
            status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
          } catch (Throwable e) {
            log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s",
                                   RemotingHelper.exceptionSimpleDesc(e),
                                   ConsumeMessageOrderlyService.this.consumerGroup,
                                   msgs,
                                   messageQueue), e);
            //抛出异常之后,设置异常标志位
            hasException = true;
          } finally {
            this.processQueue.getConsumeLock().unlock();
          }
​
          /*
          * 4.4 对返回的执行状态结果进行判断处理
          */
          //如status为null,或返回了ROLLBACK或者SUSPEND_CURRENT_QUEUE_A_MOMENT状态,那么输出日志
          if (null == status
              || ConsumeOrderlyStatus.ROLLBACK == status
              || ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
            log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}",
                     ConsumeMessageOrderlyService.this.consumerGroup,
                     msgs,
                     messageQueue);
          }
​
          //计算消费时间
          long consumeRT = System.currentTimeMillis() - beginTimestamp;
          //如status为null
          if (null == status) {
            //如果业务的执行抛出了异常
            if (hasException) {
              //设置returnType为EXCEPTION
              returnType = ConsumeReturnType.EXCEPTION;
            } else {
              //设置returnType为RETURNNULL
              returnType = ConsumeReturnType.RETURNNULL;
            }
            //如消费时间consumeRT大于等于consumeTimeout,默认15min
          } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {
            //设置returnType为TIME_OUT
            returnType = ConsumeReturnType.TIME_OUT;
            //如status为SUSPEND_CURRENT_QUEUE_A_MOMENT,即消费失败
          } else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
            //设置returnType为FAILED
            returnType = ConsumeReturnType.FAILED;
            //如status为SUCCESS,即消费成功
          } else if (ConsumeOrderlyStatus.SUCCESS == status) {
            //设置returnType为SUCCESS,即消费成功
            returnType = ConsumeReturnType.SUCCESS;
          }
​
          //如果有钩子,则将returnType设置进去
          if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
            consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());
          }
​
          //如果status为null
          if (null == status) {
            //将status设置为SUSPEND_CURRENT_QUEUE_A_MOMENT,即消费失败
            status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
          }
​
          /*
          * 4.5 如果有消费钩子,那么执行钩子函数的后置方法consumeMessageAfter
          * 我们可以注册钩子ConsumeMessageHook,在消费消息的前后调用
          */
          if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
            consumeMessageContext.setStatus(status.toString());
            consumeMessageContext
              .setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status);
            ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
          }
​
          //增加消费时间
          ConsumeMessageOrderlyService.this.getConsumerStatsManager()
            .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
​
          /*
          * K2 5 调用ConsumeMessageOrderlyService#processConsumeResult方法处理消费结果,包含重试等逻辑
          */
          continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
        } else {
          //如果没有拉取到消息,那么设置continueConsume为false,将会跳出循环
          continueConsume = false;
        }
      }
    } else {
      //如果processQueue被丢弃,则直接结束本次消费请求
      if (this.processQueue.isDropped()) {
        log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
        return;
      }
​
​
      //如果是集群模式,并且没有锁定了processQueue处理队列或者processQueue处理队列锁已经过期
      //尝试延迟加锁并重新消费
      ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
    }
  }
}
  1. 对于拉取到的消息,因为是提交给线程池操作的,所以可能存在多个线程来同时处理此消息队列,所以,这个地方,需要对消息队列进行加锁,那么可知,意味着一个消费者内消费线程池中的线程并发度是消息消费队列级别,同一个消费队列在同一时刻只会被一个线程消费,其他线程排队消费

    ini 复制代码
    final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
  1. 针对两种模式,都可以进行顺序消息的处理

    • 广播模式
    • 集群模式

      • 消费的前提:当前的消息处理队列,在消息队列分配之后,通过broker锁定判断是否锁定,只有锁定以及没有过期才能消费消息。此时是可以避免问题的。问题如下

        发生消息队列重新负载时,原先由自己处理的消息队列被另外一个消费者分配,此时如果还未来的及将ProceeQueue解除锁定,就被另外一个消费者添加进去,此时会存储多个消息消费者同时消费个消息队列,答案是不会的,因为当一个新的消费队列分配给消费者时,在添加其拉取任务之前必须先向Broker发送对该消息队列加锁请求,只有加锁成功后,才能添加拉取消息,否则等到下一次负载后,该消费队列被原先占有的解锁后,才能开始新的拉取任务

    kotlin 复制代码
    if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
                        || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
      
      //......
    }
  1. 每一个ConsumerRequest消费任务不是以消费消息条数来计算,而是根据消费时间,默认当消费时长大于60s后,就直接停止消费,其实这个地方是过滤掉一些前置判断的任务,发现已经超了规定的消费时间,那么就不要执行下面的消费操作了,因为执行肯定更加大于60s了。没必要的处理,尽量减少

    kotlin 复制代码
    //消费起始时间
    final long beginTime = System.currentTimeMillis();
    ​
    /*
    * K2 3 循环继续消费,直到超时或者条件不满足退出循环
    */
    for (boolean continueConsume = true; continueConsume; ) {
      //3.1 如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费
      if (this.processQueue.isDropped()) {
        log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
        break;
      }
    ​
      //3.2 如果是集群模式,并且没有锁定了processQueue处理队列
      if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
          && !this.processQueue.isLocked()) {
        log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
        //对该队列请求broker获取该队列的分布式锁,然后延迟提交消费请求
        ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
        break;
      }
    ​
      //3.3 如果是集群模式,并且processQueue处理队列锁已经过期
      //客户端对于从broker获取的mq锁,过期时间默认30s,可以通过-Drocketmq.client.rebalance.lockMaxLiveTime参数设置
      if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
          && this.processQueue.isLockExpired()) {
        log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
        //对该队列请求broker获取该队列的分布式锁,然后延迟提交消费请求
        ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
        //结束循环,本次消费任务结束
        break;
      }
    ​
      //计算消费时间
      long interval = System.currentTimeMillis() - beginTime;
      //3.4 如果单次消费任务的消费时间大于默认60s,可以通过-Drocketmq.client.maxTimeConsumeContinuously配置启动参数来设置时间
      if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
        //延迟提交新的消费请求
        ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
        //结束循环,本次消费任务结束
        break;
      }
      
      //。。。。。。
    }
    ​
  1. 从processQueue的MsgTree中获取到偏移量最小的数据,每次获取一批消息,此处还有一个特别的map consumingMsgOrderlyTreeMap,顺序消息消费时,从ProceessQueue中取出的消息,会临时存储在ProceeQueue的consumingMsgOrderlyTreeMap属性中。具体干什么呢,可以想想????

    kotlin 复制代码
    //获取单次批量消费的数量,默认1,可以通过DefaultMQPushConsumer.consumeMessageBatchMaxSize的属性配置
    final int consumeBatchSize =
      ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
    ​
    /*
    * 3.5 从processQueue内部的msgTreeMap有序map集合中获取offset最小的consumeBatchSize条消息,                         *    按顺序从最小的offset返回,保证有序性
    */
    List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);
    //重置重试topic,当消息是重试消息的时候,将msg的topic属性从重试topic还原为真实的topic。
    defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());

    processQueue#takeMessage

    kotlin 复制代码
    public List<MessageExt> takeMessages(final int batchSize) {
      List<MessageExt> result = new ArrayList<MessageExt>(batchSize);
      final long now = System.currentTimeMillis();
      try {
        this.treeMapLock.writeLock().lockInterruptibly();
        this.lastConsumeTimestamp = now;
        try {
          if (!this.msgTreeMap.isEmpty()) {
            for (int i = 0; i < batchSize; i++) {
              
              //只是取出了消息,但是消息仍然存在msgTreeMap中
              Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
              if (entry != null) {
                result.add(entry.getValue());
                
                //这个结构干嘛
                consumingMsgOrderlyTreeMap.put(entry.getKey(), entry.getValue());
              } else {
                break;
              }
            }
          }
    ​
          if (result.isEmpty()) {
            consuming = false;
          }
        } finally {
          this.treeMapLock.writeLock().unlock();
        }
      } catch (InterruptedException e) {
        log.error("take Messages exception", e);
      }
    ​
      return result;
    }
  1. 如果没有拉取到消息呢,直接设置标志位为

    ini 复制代码
    continueConsume=false
  1. 拉取到消息,调用监听方法进行处理,并且返回消息结果 ConsumeOrderlyStatus.SUCCESS成功或SUSPEND_CURRENT_QUEUE_A_MOMENT挂起,延迟的意思。

    ini 复制代码
    /*
    * 4.3 调用listener#consumeMessage方法,进行消息消费,调用实际的业务逻辑,返回执行状态结果
    * 有四种状态,ConsumeOrderlyStatus.SUCCESS 和 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT推荐使用
    * ConsumeOrderlyStatus.ROLLBACK和ConsumeOrderlyStatus.COMMIT已被废弃
    */
    status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
  1. 如果配置了钩子方法,就去执行钩子方法,此处也是见多不怪的扩展点
  1. 对消费结果,进行处理

    kotlin 复制代码
    //顺序消费上下文
    final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
    ​
    /*
    * K2 5 调用ConsumeMessageOrderlyService#processConsumeResult方法处理消费结果,包含重试等逻辑
    */
    continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);

    processConsumerResult中,如果消费结果为ConsumeOrderlyStatus.SUCCESS,执行ProceeQueue的commit方法,并返回待更新的消息消费进度

    scss 复制代码
    public boolean processConsumeResult(
      final List<MessageExt> msgs,
      final ConsumeOrderlyStatus status,
      final ConsumeOrderlyContext context,
      final ConsumeRequest consumeRequest
    ) {
      boolean continueConsume = true;
      long commitOffset = -1L;
      if (context.isAutoCommit()) {
        switch (status) {
          case COMMIT:
          case ROLLBACK:
            log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
                     consumeRequest.getMessageQueue());
          case SUCCESS:
            
            //IMP 调用了commit方法
            commitOffset = consumeRequest.getProcessQueue().commit();
            this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
            break;
          case SUSPEND_CURRENT_QUEUE_A_MOMENT:
            this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
            
            //检查消息的重试次数。如果消息重试次数大于或等于允许的最大重试次数,将该消息发送到Broker端,该消息在消息服务端最终会进
            //入到DLQ(死信队列),也就是RocketMQ不会再次消费,需要人工干预。如果消息成功进入到DLQ队列,checkReconsumeTimes返回
            //false,该批消息将直接调用ProcessQueue#commit提交,表示消息消费成功,如果这批消息中有任意一条消息的重试次数小于允
            //许的最大重试次数,将返回true,执行消息重试。
            if (checkReconsumeTimes(msgs)) {
              
              //消费失败了,先把此次处理的消息加到MsgTreeMap中,临时Map删除
              consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
              
              //重新构建消费请求,延迟进行消费
              this.submitConsumeRequestLater(
                consumeRequest.getProcessQueue(),
                consumeRequest.getMessageQueue(),
                context.getSuspendCurrentQueueTimeMillis());
              continueConsume = false;
            } else {
              commitOffset = consumeRequest.getProcessQueue().commit();
            }
            break;
          default:
            break;
        }
      } else {
        switch (status) {
          case SUCCESS:
            this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
            break;
          case COMMIT:
            
            commitOffset = consumeRequest.getProcessQueue().commit();
            break;
          case ROLLBACK:
            consumeRequest.getProcessQueue().rollback();
            this.submitConsumeRequestLater(
              consumeRequest.getProcessQueue(),
              consumeRequest.getMessageQueue(),
              context.getSuspendCurrentQueueTimeMillis());
            continueConsume = false;
            break;
          case SUSPEND_CURRENT_QUEUE_A_MOMENT:
            this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
            if (checkReconsumeTimes(msgs)) {
              consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
              this.submitConsumeRequestLater(
                consumeRequest.getProcessQueue(),
                consumeRequest.getMessageQueue(),
                context.getSuspendCurrentQueueTimeMillis());
              continueConsume = false;
            }
            break;
          default:
            break;
        }
      }
    ​
      if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
        this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
      }
    ​
      return continueConsume;
    }

    submitConsumeRequestLater:定时任务线程池

    java 复制代码
    /**
    * ConsumeMessageConcurrentlyService的方法
    *
    * 提交的任务被线程池拒绝,那么延迟5s进行提交,而不是丢弃
    * @param consumeRequest 提交请求
    */
    private void submitConsumeRequestLater(final ConsumeRequest consumeRequest
                                          ) {
    ​
      this.scheduledExecutorService.schedule(new Runnable() {
    ​
        @Override
        public void run() {
          //将提交的行为封装为一个线程任务,提交到scheduledExecutorService延迟线程池,5s之后执行
          ConsumeMessageConcurrentlyService.this.consumeExecutor.submit(consumeRequest);
        }
      }, 5000, TimeUnit.MILLISECONDS);

    ProcessQueue#commit

    首先申请 lockTreeMap 写锁,获取consumingMsgOrderlyTreeMap中最大的消息偏移量offset,consumingMsgOrderlyTreeMap中存放的是本批消费的消息。然后更新msgCount、msgSize,并清除 consumingMsgOrderlyTreeMap。并返回offset+1消息消费进度,从中可以看出offset表示消息消费队列的逻辑偏移量,类似于数组下标,然后调用消息进度存储器存储消息消费进度,完成该批消息的消费

    kotlin 复制代码
    public long commit() {
      try {
        this.treeMapLock.writeLock().lockInterruptibly();
        try {
          Long offset = this.consumingMsgOrderlyTreeMap.lastKey();
          msgCount.addAndGet(0 - this.consumingMsgOrderlyTreeMap.size());
          for (MessageExt msg : this.consumingMsgOrderlyTreeMap.values()) {
            msgSize.addAndGet(0 - msg.getBody().length);
          }
          
          //啊,就是临时存储一下吗
          this.consumingMsgOrderlyTreeMap.clear();
          if (offset != null) {
            return offset + 1;
          }
        } finally {
          this.treeMapLock.writeLock().unlock();
        }
      } catch (InterruptedException e) {
        log.error("commit exception", e);
      }
    ​
      return -1;
    }

总结

  1. 顺序消息有两个关键点

    1. 多个消费者同时去消费同一个队列,(顺序消息每一个消费组只能有一个去消费,需要去broker加锁
    2. 一个消费者,内部可能存在多个线程去消费消息,(此时需要针对锁定的消息队列的消息处理队列进行加锁,针对消费线程)
  2. PullMessageService和ConsumerMessageService之间的消息互通,主要是通过ProcessQueue来进行交互的

相关推荐
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码9 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries9 小时前
读《show your work》的一点感悟
后端
A尘埃9 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23079 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code9 小时前
(Django)初步使用
后端·python·django
代码之光_19809 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端