RocketMQ 源码学习--Consumer-01 整体流程学习

带着问题去研究中间件,想想自己实现如何实现

前提

通过架构可以知道下面角色之间的对应关系

  1. 主题:消息队列(MessageQueue)= 1:n
  2. 主题:消息生产者 = 1:n (n>=1)
  3. 主题:消息消费者 = 1:n(n>=1)

问题

围绕着上面的关系那么就会存在三类问题

消费者

  1. 消费组角度:一个消费组中多个消费者是如何对消息队列(1个主题多个消息队列)进行负载消费的。
  2. 消费者角度:一个消费者中多个线程又是如何协作(并发)的消费分配给该消费者的消息队列中的消息呢?
  3. 增量消费方面:消息消费进度如何保存,包括MQ是如何知道消息是否正常被消费了。

重要的类

不熟悉的话,可以边看变回来查看具体的功能

  • DefaultMQPushConsumerImpl :消息消息者默认实现类,应用程序中直接用该类的实例完成消息的消费,并回调业务方法。
  • RebalanceImpl 字面上的意思(重新平衡)也就是消费端消费者与消息队列的重新分布,与消息应该分配给哪个消费者消费息息相关。
  • MQClientInstance 消息客户端实例,负载与MQ服务器(Broker,Nameserver)交互的网络实现
  • PullAPIWrapper Pull与Push在RocketMQ中,其实就只有Pull模式,所以Push其实就是用pull封装一下
  • MessageListenerInner 消费消费回调类,当消息分配给消费者消费时,执行的业务代码入口
  • OffsetStore 消息消费进度保存
  • ConsumeMessageService 消息消费逻辑

源码

Consumer 启动

入口:DefaultMQPushConsumer 的 start 方法

RebalanceImpl启动

依赖的 MQClientFactory 的初始化

kotlin 复制代码
//IMP 决定了后面的 MQInstance  也就是与其他组件的交换逻辑
//如果是集群消费模式,如果instanceName为默认值 "DEFAULT",那么改成 UtilAll.getPid() + "#" + System.nanoTime()
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
  this.defaultMQPushConsumer.changeInstanceNameToPID();
}
​
/*
* K2 3 获取MQClientManager实例,然后根据clientId获取或者创建CreateMQClientInstance实例,并赋给mQClientFactory变量
*
* MQClientInstance封装了RocketMQ底层网络处理API,Producer、Consumer都会使用到这个类,是Producer、Consumer与NameServer、Broker 打交道的网络通道。
* 因此,同一个clientId对应同一个MQClientInstance实例就可以了,即同一个应用中的多个producer和consumer使用同一个MQClientInstance实例即可。
*/
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

rebalanceImpl初始化,主要其中注入了 MQClientFactory,那岂不是一个 PushConsuemr 实例,必然有一个次对象,这个对象的功能,后续说明

kotlin 复制代码
/*
* K2 4 设置负载均衡服务的相关属性
*   RebalanceImpl 要解决的问题:对 MessageQueue 资源的重平衡
*/
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
        this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

PullAPIWrapper 对象构建

此对象的构建中,也存在一个 mQClientFactory

kotlin 复制代码
/*
* IMP 核心组件,无论是推还是拉都是使用此组件来执行的
* K2 5 创建消息拉取核心对象PullAPIWrapper,封装了消息拉取及结果解析逻辑的API0º
*/
this.pullAPIWrapper = new PullAPIWrapper(
  mQClientFactory,
  this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
  
//为PullAPIWrapper注册过滤消息的钩子函数
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

OffsetStore 对象构建

同样的也 MQClientFactory 注入到对象中,同时此处,针对不同的消费方式,消息存储在不同的地方本地和远端 Broker( 如果 Broker 不进行通信的话,岂不是会丢失进度呢???

kotlin 复制代码
/* IMP OffsetStore 是用于记录当前消费者消费进度的一个组件
*   LocalFileOffsetStore:顾名思义,就是将消费进度存储在 Consumer 本地,Consumer 会在磁盘上生成文件以保存进度。
*   RemoteBrokerOffsetStore:将消费进度保存在远端的 Broker。
*
* K2 6 根据消息模式设置不同的OffsetStore,用于实现消费者的消息消费偏移量offset的管理
*/
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
  this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
  //根据不用的消费模式选择不同的OffsetStore实现
  switch (this.defaultMQPushConsumer.getMessageModel()) {
    case BROADCASTING:
      //如果是广播消费模式,则是LocalFileOffsetStore,消息消费进度即offset存储在本地磁盘中。
      this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
      break;
    case CLUSTERING:
      //如果是集群消费模式,则是RemoteBrokerOffsetStore,消息消费进度即offset存储在远程broker中。
      this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
      break;
    default:
      break;
  }
  this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}

ConsumeMessageService 消费管理构建

kotlin 复制代码
/*
* K2 8 根据消息监听器的类型创建不同的消息消费服务
*/
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
  //如果是MessageListenerOrderly类型,则表示顺序消费,创建ConsumeMessageOrderlyService
  this.consumeOrderly = true;
  this.consumeMessageService =
    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
  //如果是MessageListenerConcurrently类型,则表示并发消费,创建ConsumeMessageOrderlyService
  this.consumeOrderly = false;
  this.consumeMessageService =
    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently)    this.getMessageListenerInner());  
}
​
//启动消息消费服务
this.consumeMessageService.start();

MQClientInstance 启动

  • 里面干的事情,各种定时任务之类的,同时这个对象里面包含了很多重要对象,包含了,各种消费者对象,路由信息,broker 信息,以及拉取服务等等,

定时任务

  • 具体任务如下

PullMessageService 启动

  • 是一个现成,那么肯定是一个自旋的任务去执行
kotlin 复制代码
@Override
public void run() {
  log.info(this.getServiceName() + " service started");
​
  // 自旋
  while (!this.isStopped()) {
    try {
      PullRequest pullRequest = this.pullRequestQueue.take();
      
      //拉取任务
      this.pullMessage(pullRequest);
    } catch (InterruptedException ignored) {
    } catch (Exception e) {
      log.error("Pull Message Service Run Method exception", e);
    }
  }
​
  log.info(this.getServiceName() + " service end");
}
java 复制代码
private void pullMessage(final PullRequest pullRequest) {
  //获取消费者
  final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
  if (consumer != null) {
    DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
    impl.pullMessage(pullRequest);
  } else {
    log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
  }
}
csharp 复制代码
public MQConsumerInner selectConsumer(final String group) {
  return this.consumerTable.get(group);
}
  • 上面说明了啥呢,cnsumerTable 是一个 Map结构,如果一个应用创建了同一个消费者组的多个消费者,此处会怎样呢,必然只会选择一个,所以,同一个应用增加多个消费者并不会 提高消费效率。但是实际上看了,对象而言,这个 consumerTable 是私有变量,根本不会进行重用,应该两个都会消费的,这个地方应该有什么问题,请教一下 大佬问问
  • 看到这里,终于知道了,其实内部还是启动的其他线程(PullMessageService)来执行拉取消息。那么 Consumer 和 PullMessageService 的关系是什么样子的呢?

    • 一个应用程序(消费端),一个消费组 一个 DefaultMQPushConsumerImpl ,同一个IP:端口,会有一个MQClientInstance ,而每一个MQClientInstance中持有一个PullMessageServive实例,故可以得出如下结论:同一个应用程序中,如果存在多个消费组,那么多个DefaultMQPushConsumerImpl 的消息拉取,都需要依靠一个PullMessageServive
    • 简言之就是,一个 Consumer 对应一个 MQClientIntstance,也对应一个 PullMessageService,不同的 Consumer 对应的 PullMessageService 不一样。和大佬沟通之后,发现,之前的版本都是Producer 和 Consumer 都是底层使用同一个 MQClientInstance,但是呢,现在都是一对一的,即一个 Consumer 使用一个 MQClientInstance,也就底层一个 PullMessageService 了。终于解惑了。。。

PullMessageService 是依赖内存队列的请求进行拉取消息的,那么这个请求是什么时候加入到这个对象里面的呢???

PullMessageService 拉取消息

  • 上面的循环不断的从队列中获取 PullRequest

    java 复制代码
    private void pullMessage(final PullRequest pullRequest) {
      final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
      if (consumer != null) {
        DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        impl.pullMessage(pullRequest);
      } else {
        log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
      }
    }
  • 上面的拉取消息,最终回到 DefaultMQPushConsumerImpl的pullMessage

DefualtMQPushConsumerImpl 的 pullMessage 方法

  1. 首先获取PullRequest的 处理队列ProcessQueue,然后更新该消息队列最后一次拉取的时间。
scss 复制代码
// IMP
//  ProcessQueue 内部会通过 TreeMap 来存放这些暂时还没有被消费的 Message,TreeMap 是一个用红黑树实现的有序 Map。
//  Key 是消息在当前 ProcessQueue 所对应的 MessageQueue 中的偏移量,Value 就是 Message 自身。
final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
  log.info("the pull request[{}] is dropped.", pullRequest.toString());
  return;
}
​
​
​
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
  1. 如果消费者 服务状态不为ServiceState.RUNNING,或当前处于暂停状态,默认延迟3秒再执行(PullMessageService.executePullRequestLater)。 如何实现延迟执行呢,简单就使用 Sleep 方法。等会看看里面具体如何实现延迟的操作。
kotlin 复制代码
try {
  this.makeSureStateOK();
} catch (MQClientException e) {
  log.warn("pullMessage exception, consumer state not ok", e);
  
  //延迟执行
  this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
  return;
}
​
//暂停
if (this.isPause()) {
  log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
  return;
}

延迟消息实现

  • 使用定时任务线程池,然后过了多久之后,重新放回到 PullRequest 中,可见这个延迟也是个大概的时间,具体需要等消费者也就是 PullMessageService 拉取到才去执行

    java 复制代码
    public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) {
      if (!isStopped()) {
        this.scheduledExecutorService.schedule(new Runnable() {
          @Override
          public void run() {
            PullMessageService.this.executePullRequestImmediately(pullRequest);
          }
        }, timeDelay, TimeUnit.MILLISECONDS);
      } else {
        log.warn("PullMessageServiceScheduledThread has shutdown");
      }
    }
    ​
    ​
    ​
    //PullMessageService.executePullRequestImmediately
    /**
    * PullMessageService的方法
    * 下一次消息拉取
    *
    * @param pullRequest 拉取请求
    */
    public void executePullRequestImmediately(final PullRequest pullRequest) {
      try {
        //存入pullRequestQueue集合,等待下次拉取
        this.pullRequestQueue.put(pullRequest);
      } catch (InterruptedException e) {
        log.error("executePullRequestImmediately pullRequestQueue.put", e);
      }
    }
  1. 拉取消息进行限流限速

    1. 消息数量达到阔值(默认1000个)
    2. 消息体总大小(默认100m)
scss 复制代码
long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
​
// 数量
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  if ((queueFlowControlTimes++ % 1000) == 0) {
    log.warn(
      "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
      this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
  }
  return;
}
​
//消息体大小
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  if ((queueFlowControlTimes++ % 1000) == 0) {
    log.warn(
      "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
      this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
  }
  return;
}

非顺序消息拉取

  • 如果我们自己实现的话,肯定是,获取订阅的 broker 地址信息(通过版本进行增量拉取,同时设置最大拉取次数,防止,过多拉取失败,导致消费太慢),然后去 broker 中拉取消息,如果拉取失败了,进行重试处理。看看 MQ 是如何实现的呢。
  1. 获取主题订阅信息

    kotlin 复制代码
    final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
    if (null == subscriptionData) {
      this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
      log.warn("find the consumer's subscription failed, {}", pullRequest);
      return;
    }
    • 主题的订购信息,实体类

      vbnet 复制代码
      public class SubscriptionData implements Comparable<SubscriptionData> {
          public final static String SUB_ALL = "*";
          private boolean classFilterMode = false;
          private String topic;
          private String subString;
          private Set<String> tagsSet = new HashSet<String>();
          private Set<Integer> codeSet = new HashSet<Integer>();
          private long subVersion = System.currentTimeMillis();
          private String expressionType = ExpressionType.TAG;
      }
  1. 构造回调方法,涉及到重试操作

  2. 获取偏移量,好吧,那上面的当前拉取时间是干嘛用的呢???如果不记得可以返回去看看 setLastPullTimestamp

    1. 如果是集群模式,就去内存中获取偏移量

      ini 复制代码
      long commitOffsetValue = 0L;
      if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
        
        //此处会涉及本地获取还是集群获取
        commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
        if (commitOffsetValue > 0) {
          commitOffsetEnable = true;
        }
      }
  3. 拉取消息,好嘛,最后还是交给了别人来进行拉取,自己就是构造了请求参数等等信息,不过也是,专业的事儿找专业的人来干,后续可以很好的扩展

    less 复制代码
    //IMP 拉取消息
    this.pullAPIWrapper.pullKernelImpl(
      pullRequest.getMessageQueue(),
      subExpression,
      subscriptionData.getExpressionType(),
      subscriptionData.getSubVersion(),
      pullRequest.getNextOffset(),
      this.defaultMQPushConsumer.getPullBatchSize(),
      sysFlag,
      commitOffsetValue,
      BROKER_SUSPEND_MAX_TIME_MILLIS,
      CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
      CommunicationMode.ASYNC,
      pullCallback
    );
  4. 就是进一步获取 topic 和 broker 的地址信息,然后交由 MQClientAPIImpl 进行拉取消息

    java 复制代码
    //MQClientAPIImpl.pullMessage
      /**
    * IMP 拉取消息
    * @param addr
    * @param requestHeader
    * @param timeoutMillis
    * @param communicationMode
    * @param pullCallback
    * @return
    * @throws RemotingException
    * @throws MQBrokerException
    * @throws InterruptedException
    */
    public PullResult pullMessage(
      final String addr,
      final PullMessageRequestHeader requestHeader,
      final long timeoutMillis,
      final CommunicationMode communicationMode,
      final PullCallback pullCallback
    ) throws RemotingException, MQBrokerException, InterruptedException {
      RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
    ​
      switch (communicationMode) {
        case ONEWAY:
          assert false;
          return null;
        case ASYNC:
          this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
          return null;
        case SYNC:
          return this.pullMessageSync(addr, request, timeoutMillis);
        default:
          assert false;
          break;
      }
    ​
      return null;
    }
    • 对于异步和同步而言,同步是等待返回结果,而异步,就是在 DefaultConsumerPushImpl 拉取消息的时候,创建的回调函数进行处理,其实这也是异步的常用套路。使用回调来进行结果处理

      java 复制代码
      private void pullMessageAsync(
        final String addr,
        final RemotingCommand request,
        final long timeoutMillis,
        final PullCallback pullCallback
      ) throws RemotingException, InterruptedException {
      ​
        /*
        * 基于netty给broker发送异步消息,设置一个InvokeCallback回调对象
        *
        * InvokeCallback#operationComplete方法将会在得到结果之后进行回调,内部调用pullCallback的回调方法
        */
        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
      ​
          /**
          * 异步执行的回调方法
          */
          @Override
          public void operationComplete(ResponseFuture responseFuture) {
      ​
            //返回命令对象
            RemotingCommand response = responseFuture.getResponseCommand();
            if (response != null) {
              try {
                //解析响应获取结果
                PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr);
                assert pullResult != null;
                //如果解析到了结果,那么调用pullCallback#onSuccess方法处理
                pullCallback.onSuccess(pullResult);
              } catch (Exception e) {
                //出现异常则调用pullCallback#onException方法处理异常
                pullCallback.onException(e);
              }
            } else {
              //没有结果,都调用onException方法处理异常
              if (!responseFuture.isSendRequestOK()) {
                //发送失败
                pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
              } else if (responseFuture.isTimeout()) {
                //超时
                pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
                                                               responseFuture.getCause()));
              } else {
                pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
              }
            }
          }
        });
      }
  5. 回调方法

    1. 这个时候是不是想到几个点,如果你自己写的时候,

      1. 不同响应如何处理,成功,失败,异常
      2. 内存队列进行消费,如果一个拉取请求太久怎么办,或者 broker 超时了,怎么去重试,要求肯定是不能阻塞后续的消费
    kotlin 复制代码
    PullCallback pullCallback = new PullCallback() {
      @Override
      public void onSuccess(PullResult pullResult) {
        if (pullResult != null) {
    ​
          /*
          * K2 1 处理pullResult,进行消息解码、过滤以及设置其他属性的操作
          */
          pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                                                                                       subscriptionData);
    ​
          switch (pullResult.getPullStatus()) {
            case FOUND:
              //拉取的起始offset
              long prevRequestOffset = pullRequest.getNextOffset();
              //设置下一次拉取的起始offset到PullRequest中
              pullRequest.setNextOffset(pullResult.getNextBeginOffset());
              //增加拉取耗时
              long pullRT = System.currentTimeMillis() - beginTimestamp;
              DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                                                                                 pullRequest.getMessageQueue().getTopic(), pullRT);
    ​
              long firstMsgOffset = Long.MAX_VALUE;
    ​
              //如果没有消息
              if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                /*
                * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,PullMessageService是一个线程服务
                * PullMessageService将会循环的获取pullRequestQueue中的pullRequest然后向broker发起新的拉取消息请求
                * 进行下次消息的拉取
                */
                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
              } else {
                //获取第一个消息的offset
                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
    ​
                //增加拉取tps
                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                                                                    pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
    ​
                /*
                * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中
                */
                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
    ​
                /*
                * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息
                *   consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现
                */
                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                  pullResult.getMsgFoundList(),
                  processQueue,
                  pullRequest.getMessageQueue(),
                  dispatchToConsume);
    ​
    ​
                /*
                * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中
                *      进行下次消息的拉取
                */
                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    ​
                  /*
                  * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中
                  * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法
                  */
                  DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                                                         DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                } else {
                  /*
                  * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取
                  */
                  DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                }
              }
    ​
              if (pullResult.getNextBeginOffset() < prevRequestOffset
                  || firstMsgOffset < prevRequestOffset) {
                log.warn(
                  "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                  pullResult.getNextBeginOffset(),
                  firstMsgOffset,
                  prevRequestOffset);
              }
    ​
              break;
            case NO_NEW_MSG:
              //没有匹配到消息
    ​
            case NO_MATCHED_MSG:
              //更新下一次拉取偏移量
              pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    ​
              DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
    ​
              //立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取
              DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
              break;
    ​
              //请求offset不合法,过大或者过小
            case OFFSET_ILLEGAL:
              log.warn("the pull request offset illegal, {} {}",
                       pullRequest.toString(), pullResult.toString());
    ​
              //更新下一次拉取偏移量,这个下一次的开始偏移是broker那边进行返回的
              pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    ​
              //丢弃拉取请求
              pullRequest.getProcessQueue().setDropped(true);
              DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
    ​
                @Override
                public void run() {
                  try {
    ​
                    //更新下次拉取偏移量
                    DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
                                                                            pullRequest.getNextOffset(), false);
                    //持久化offset
                    DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
                    //移除对应的消费队列,同时将消息队列从负载均衡服务中移除
                    DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
    ​
                    log.warn("fix the pull request offset, {}", pullRequest);
                  } catch (Throwable e) {
                    log.error("executeTaskLater Exception", e);
                  }
                }
              }, 10000);
              break;
            default:
              break;
          }
        }
      }
    ​
      /*
      * 出现异常,延迟3s将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取
      */
      @Override
      public void onException(Throwable e) {
        if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
          log.warn("execute the pull request exception", e);
        }
    ​
        DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
      }
    };
    • 上面无非就是针对不同的情况,来进行相应的处理,下面主要针对成功和异常的情况看看,成功之后如何处理,异常之后如何重试
  6. 拉取成功之后的处理

    kotlin 复制代码
    //获取第一个消息的offset
    firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
    ​
    //增加拉取tps
    DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                                                        pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
    ​
    /*
    * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中
    */
    boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
    ​
    /*
    * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息
    *   consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现
    */
    DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
      pullResult.getMsgFoundList(),
      processQueue,
      pullRequest.getMessageQueue(),
      dispatchToConsume);
    ​
    ​
    /*
    * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中
    *      进行下次消息的拉取
    */
    if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    ​
    /*
    * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中
    * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法
    */
      DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                                             DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
    } else {
      /*
     * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取
      */
      DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    }
    }
    ​
    if (pullResult.getNextBeginOffset() < prevRequestOffset
        || firstMsgOffset < prevRequestOffset) {
      log.warn(
        "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
        pullResult.getNextBeginOffset(),
        firstMsgOffset,
        prevRequestOffset);
    }
    ​
    break;
    1. 拉取到消息之后,放到消费队列 processQueue,这还这么操作,拉取到难道不消费吗,各种内存队列,生产-消费者模式呀,生产者是当前的消费者 PushConsumer,内存队列就是加入的地方,那么消费者是谁呢,如何通知消费者来进行消费消息呢,后续可以看到,此方法中底层代码可以看到将消息加入一个 msgTreeMap 中,同时增加了一些统计数据

      ini 复制代码
      /**
      * IMP putMessage
      *
      *   该方法将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中。
      *
      *   返回是否需要分发消费dispatchToConsume,当当前processQueue的内部的msgTreeMap中
      *   有消息并且consuming=false,即还没有开始消费时,将会返回true。
      *
      * dispatchToConsume对并发消费无影响,只对顺序消费有影响。
      * @param msgs 一批消息
      * @return 是否需要分发消费,当当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,将会返回true
      */
      public boolean putMessage(final List<MessageExt> msgs) {
        boolean dispatchToConsume = false;
        try {
          //尝试加写锁防止并发
          this.treeMapLock.writeLock().lockInterruptibly();
          try {
            int validMsgCnt = 0;
            for (MessageExt msg : msgs) {
              //当该消息的偏移量以及该消息存入msgTreeMap
              // 此处肯定是两个数据结构的关联点
              MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg);
              if (null == old) {
                //如果集合没有这个offset的消息,那么增加统计数据
                validMsgCnt++;
                this.queueOffsetMax = msg.getQueueOffset();
                msgSize.addAndGet(msg.getBody().length);
              }
            }
            //消息计数
            msgCount.addAndGet(validMsgCnt);
      ​
            //当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,dispatchToConsume = true,consuming = true
            if (!msgTreeMap.isEmpty() && !this.consuming) {
              dispatchToConsume = true;
              this.consuming = true;
            }
      ​
            //计算broker累计消息数量
            if (!msgs.isEmpty()) {
              MessageExt messageExt = msgs.get(msgs.size() - 1);
              String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET);
              if (property != null) {
                long accTotal = Long.parseLong(property) - messageExt.getQueueOffset();
                if (accTotal > 0) {
                  this.msgAccCnt = accTotal;
                }
              }
            }
          } finally {
            this.treeMapLock.writeLock().unlock();
          }
        } catch (InterruptedException e) {
          log.error("putMessage exception", e);
        }
      ​
        return dispatchToConsume;
      }
    2. 消费消息服务提交(只看非顺序的)

      1. 上面是降拉取到 的消息存储到了 treeMap 中,其实如果这个接口是阻塞队列的话,是可以接受一个线程不断自旋获取,或者阻塞等待获取,如果有消息那么就去消费,但是通过源码分析,是利用 treeMap 的有序性,以及查询速度快的情况来作为存储消息的结构,那么就需要一个阻塞队列,来通知,消费者来进行消费,所以,此处就会有消息服务提交的一个步骤。
      2. 通过下面源码发现,是直接将 返回的消息,构造成一个 runnable 然后交给了线程池执行,底层也是一种生产-消费模式。有点奇怪了,那上面的 msgTreeMap 存储消息干嘛呢???
      java 复制代码
      @Override
      public void submitConsumeRequest(
        final List<MessageExt> msgs,
        final ProcessQueue processQueue,
        final MessageQueue messageQueue,
        final boolean dispatchToConsume) {
      ​
        //单次批量消费的数量,默认1
        //   consumeMessageBatchMaxSize是什么意思呢?他的字面意思就是单次批量消费的数量,实际上它代表着每次发送给
        //   消息监听器MessageListenerOrderly或者MessageListenerConcurrently的consumeMessage方法中的参数List msgs中的最多的消息数量。
        //
        //   consumeMessageBatchMaxSize默认值为1,所以说,无论是并发消费还是顺序消费,每次的consumeMessage方法的执行,
        //   msgs集合默认都只有一条消息。同理,如果把它设置为其他值n,无论是并发消费还是顺序消费,每次的consumeMessage的执行,msgs集合默认都最多只有n条消息。
        //
        final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
      ​
        /*
        * 如果消息数量 <= 单次批量消费的数量,那么直接全量消费
        */
        if (msgs.size() <= consumeBatchSize) {
      ​
          //构建消费请求,将消息全部放进去
          ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
          try {
            
            // 重点
            //将请求提交到consumeExecutor线程池中进行消费
            this.consumeExecutor.submit(consumeRequest);
          } catch (RejectedExecutionException e) {
            //提交的任务被线程池拒绝,那么延迟5s进行提交,而不是丢弃
            this.submitConsumeRequestLater(consumeRequest);
          }
        }
        /*
        * 如果消息数量 > 单次批量消费的数量,那么需要分割消息进行分批提交
        */
        else {
          //遍历
          for (int total = 0; total < msgs.size(); ) {
            //一批消息集合,每批消息最多consumeBatchSize条,默认1
            List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
      ​
            //将消息按顺序加入集合
            for (int i = 0; i < consumeBatchSize; i++, total++) {
              if (total < msgs.size()) {
                msgThis.add(msgs.get(total));
              } else {
                break;
              }
            }
      ​
            //将本批次消息构建为ConsumeRequest
            ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
            try {
      ​
              //将请求提交到consumeExecutor线程池中进行消费
              this.consumeExecutor.submit(consumeRequest);
            } catch (RejectedExecutionException e) {
              //被拒绝之后,把所有的消息都加入到的msgThis这个集合中,整体延迟5s进行执行
              for (; total < msgs.size(); total++) {
                msgThis.add(msgs.get(total));
              }
              //提交的任务被线程池拒绝,那么所有后面的任务都延迟5s进行提交,而不是丢弃
              this.submitConsumeRequestLater(consumeRequest);
            }
          }
        }
      }
      • 每次超了每次消费的消息个数,那么就对消息进行分批处理,然后依次处理,不过这个地方写的,和我们写的也没啥区别

ConsumeRequest

单独拎出来是比较重要,也是每一批消息,消费的任务模型,直接上源码,又臭又长,还是慢慢分析吧,因为是 Runnable,懂得都懂

  1. 既然消费一批消息, 即 processQueue,那么一开始的一些校验肯定必不可少了,如果是重试消息的话,还原会真正的主题,咦??,所以,重试消息的主题在拉取到之后,broker 端不进行修改主题信息吗,为啥此处还有进行调整呢???后续重试消息的地方讲解

    kotlin 复制代码
    //如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费
    if (this.processQueue.isDropped()) {
      log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
      return;
    }
    ​
    //重置重试topic
    defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
    arduino 复制代码
    public void resetRetryAndNamespace(final List<MessageExt> msgs, String consumerGroup) {
      //获取重试topic
      final String groupTopic = MixAll.getRetryTopic(consumerGroup);
      for (MessageExt msg : msgs) {
        //尝试通过PROPERTY_RETRY_TOPIC属性获取每个消息的真实topic
        String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
    ​
        //如果该属性不为null,并且重试topic和消息的topic相等,则表示当前消息是重试消息
        if (retryTopic != null && groupTopic.equals(msg.getTopic())) {
          //那么设置消息的topic为真实topic,即还原回来
          msg.setTopic(retryTopic);
        }
    ​
        if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) {
          msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
        }
      }
    }
  2. 消费消息前的扩展点,钩子函数

    ini 复制代码
    /*
    * K2 2 如果有消费钩子,那么执行钩子函数的前置方法consumeMessageBefore
    *   我们可以注册钩子ConsumeMessageHook,再消费消息的前后调用
    */
    ConsumeMessageContext consumeMessageContext = null;
    if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
      consumeMessageContext = new ConsumeMessageContext();
      consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
      consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup());
      consumeMessageContext.setProps(new HashMap<String, String>());
      consumeMessageContext.setMq(messageQueue);
      consumeMessageContext.setMsgList(msgs);
      consumeMessageContext.setSuccess(false);
      ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
    }
  3. 调用业务的监听器方法来处理消息

    less 复制代码
    try {
      if (msgs != null && !msgs.isEmpty()) {
    ​
        //循环设置每个消息的起始消费时间
        for (MessageExt msg : msgs) {
          MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
        }
      }
    ​
      /*
      * K2 3 调用listener#consumeMessage方法,进行消息消费,调用实际的业务逻辑,返回执行状态结果
      * 有两种状态ConsumeConcurrentlyStatus.CONSUME_SUCCESS 和 ConsumeConcurrentlyStatus.RECONSUME_LATER
      */
      status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
    } catch (Throwable e) {
      log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s",
                             RemotingHelper.exceptionSimpleDesc(e),
                             ConsumeMessageConcurrentlyService.this.consumerGroup,
                             msgs,
                             messageQueue), e);
      //抛出异常之后,设置异常标志位
      hasException = true;
    }
  4. 根据不同的消费状态 status 进行 处理,主要还是去判断,超时了,异常的情况,如何去处理呢

    ini 复制代码
    /*
    * K2 4 对返回的执行状态结果进行判断处理
    */
    //计算消费时间
    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为RECONSUME_LATER,即消费失败
    } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) {
      //设置returnType为FAILED
      returnType = ConsumeReturnType.FAILED;
    ​
      //如status为CONSUME_SUCCESS,即消费成功
    } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) {
      //设置returnType为SUCCESS,即消费成功
      returnType = ConsumeReturnType.SUCCESS;
    }
    ​
    ​
    //兜底策略,默认返回,没有返回的话,就认为是消费失败,等会在消费
    //如果status为null
    if (null == status) {
      log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}",
               ConsumeMessageConcurrentlyService.this.consumerGroup,
               msgs,
               messageQueue);
      //将status设置为RECONSUME_LATER,即消费失败
      status = ConsumeConcurrentlyStatus.RECONSUME_LATER;
    }
  5. 如果有钩子,后续执行钩子方法

    ini 复制代码
    /*
    * K2 5 如果有消费钩子,那么执行钩子函数的后置方法consumeMessageAfter
    *      我们可以注册钩子ConsumeMessageHook,在消费消息的前后调用
    */
    if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
      consumeMessageContext.setStatus(status.toString());
      consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status);
      ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
    }
  6. 上面记录的消息消费之后的,状态, 但是针对不同的结果之后,没有具体的处理策略,显然不合适,只是执行了,消费前后的钩子方法,那么真正的处理方法如下,也是针对,成功,超时,异常,重试的后续一些操作,为啥要写一个方法呢,因为后续的特别多的逻辑需要处理,这样分开来说,每个方法指责不同,而且方便单侧

    kotlin 复制代码
    /*
    * K2 6 如果处理队列没有被丢弃,那么调用ConsumeMessageConcurrentlyService#processConsumeResult方法处理消费结果,包含重试等逻辑
    */
    if (!processQueue.isDropped()) {
      ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
    } else {
      log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
    }

总结

  1. 上面通过源码看到了,Consumer 启动的时候,启动了哪些数据

  2. Consumer 中各种生产-消费模式进行获取消息,消费消息,涉及组件

    1. Consumer->PullMessageService(Thread,自旋获取PullRequest)->offStore->CosumerMessageService( 内部含有ThreadPool 进行异步执行)

遗留问题

对于消费端

  1. PullRequest是哪里产生的呢???
  2. 消费端消息负载均衡机制与重新分布
  3. MsgTreeMap 干嘛用的,为何要保存依次数据
  4. 消息消费进度保持机制

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

相关推荐
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风4 小时前
详解K8S--声明式API
后端
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~5 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616885 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
睡觉谁叫~~~6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
2401_865854888 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
AskHarries8 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622668 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化