RocketMQ 源码学习--Consumer-01 整体流程 & 重复消费

697 阅读33分钟

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

前提

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

  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 的初始化

//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 实例,必然有一个次对象,这个对象的功能,后续说明

/*
* 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

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

OffsetStore 对象构建

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

/* 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 消费管理构建

/*
* 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 启动

img

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

定时任务

  • 具体任务如下

image.png

PullMessageService 启动

  • 是一个现成,那么肯定是一个自旋的任务去执行
@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");
}
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);
  }
}
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

    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,然后更新该消息队列最后一次拉取的时间。
// 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 方法。等会看看里面具体如何实现延迟的操作。
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 拉取到才去执行

    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)
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. 获取主题订阅信息

    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;
    }
    
    • 主题的订购信息,实体类

      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. 如果是集群模式,就去内存中获取偏移量

      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. 拉取消息,好嘛,最后还是交给了别人来进行拉取,自己就是构造了请求参数等等信息,不过也是,专业的事儿找专业的人来干,后续可以很好的扩展

    //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 进行拉取消息

    //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 拉取消息的时候,创建的回调函数进行处理,其实这也是异步的常用套路。使用回调来进行结果处理

      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 超时了,怎么去重试,要求肯定是不能阻塞后续的消费
    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. 拉取成功之后的处理

    //获取第一个消息的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 中,同时增加了一些统计数据

      /**
      * 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 存储消息干嘛呢???
      @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 端不进行修改主题信息吗,为啥此处还有进行调整呢???后续重试消息的地方讲解

    //如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费
    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());
    
    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. 消费消息前的扩展点,钩子函数

    /*
    * 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. 调用业务的监听器方法来处理消息

    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 进行 处理,主要还是去判断,超时了,异常的情况,如何去处理呢

    /*
    * 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. 如果有钩子,后续执行钩子方法

    /*
    * 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. 上面记录的消息消费之后的,状态, 但是针对不同的结果之后,没有具体的处理策略,显然不合适,只是执行了,消费前后的钩子方法,那么真正的处理方法如下,也是针对,成功,超时,异常,重试的后续一些操作,为啥要写一个方法呢,因为后续的特别多的逻辑需要处理,这样分开来说,每个方法指责不同,而且方便单侧

    /*
    * 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);
    }
    

processConsumeResult

涉及:消息消费重试机制

从方法名称就可以得知,次方法主要是用于处理消费结果的,针对消费结果的响应,进一步处理

/*
* K2 1 判断消费状态,设置ackIndex的值
* 消费成功: ackIndex = 消息数量 - 1
* 消费失败: ackIndex = -1
*/
switch (status) {
    //如果消费成功
  case CONSUME_SUCCESS:
​
    //限制回复的上限,因为当前还在处理一条数据,所以认为之前的量就是-1 条的消息
    //如果大于等于消息数量,则设置为消息数量减1
    //初始值为Integer.MAX_VALUE,因此一般都会设置为消息数量减1
    if (ackIndex >= consumeRequest.getMsgs().size()) {
      ackIndex = consumeRequest.getMsgs().size() - 1;
    }
​
    //消费成功的个数,即消息数量
    int ok = ackIndex + 1;
    //消费失败的个数,即0
    int failed = consumeRequest.getMsgs().size() - ok;
    //统计
    this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
    break;
    //如果消费失败
  case RECONSUME_LATER:
​
    //决定起始索引
    //ackIndex初始化为-1
    ackIndex = -1;
    //统计
    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
                                                       consumeRequest.getMsgs().size());
    break;
  default:
    break;
}
  • 上面的操作,就记录 ackIndex,可以理解成,哪些消息消费成功,很显然,-1 代表了所有的没有消费成功,具体可见后面的逻辑
/*
* K2 2 判断消息模式,处理消费失败的情况
* 广播模式:打印日志
* 集群模式:向broker发送当前消息作为延迟消息,等待重试消费
*/
switch (this.defaultMQPushConsumer.getMessageModel()) {
    //广播模式下
  case BROADCASTING:
    //从消费成功的消息在消息集合中的索引+1开始,仅仅是对于消费失败的消息打印日志,并不会重试
    for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
      MessageExt msg = consumeRequest.getMsgs().get(i);
      log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
    }
    break;
    //集群模式下
  case CLUSTERING:
    List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
    //消费成功的消息在消息集合中的索引+1开始,遍历消息
    for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
​
      //针对每条信息进行依次处理
      //获取该索引对应的消息
      MessageExt msg = consumeRequest.getMsgs().get(i);
​
      /*
      * 2.1 消费失败后,将该消息重新发送至重试队列,延迟消费
      */
      boolean result = this.sendMessageBack(msg, context);
      //如果执行发送失败
      if (!result) {
        //设置重试次数+!
        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
        //加入失败的集合
        msgBackFailed.add(msg);
      }
    }
​
​
    // 回复 ack 失败的消息,直接在本地进行执行就好了,防止 broker 坏了,消息无法进行消费
    if (!msgBackFailed.isEmpty()) {
      //从consumeRequest中移除消费失败并且发回broker失败的消息
      consumeRequest.getMsgs().removeAll(msgBackFailed);
      /*
      * 2.2 调用submitConsumeRequestLater方法,延迟5s将sendMessageBack执行失败的消息再次提交到consumeExecutor进行消费
      */
      this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
    }
    break;
  default:
    break;
}
  1. CONSUME_SUCCESS:

    1. ackIndex = msg.size()-1,ackIndex + 1即所有的消息消费成,也就不需要重新消费
  2. RECONSUME_LATER

    1. ackIndex = -1,for (int i = ackIndex + 1;意味着,从第一条数据开始全部消费失败

      1. 【sendMessageBack】,延迟 5s 后重新在消费端重新消费

      2. sendMessageBack消费失败

        1. 直接在本地处理该消息,默认是延迟 5s 后进行消费,怎么理解呢,既然发送不成功,那么就默认是消费成功,就在本地重新消费一下试试【submitConsumeRequestLater】
sendMessageBack
public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName)
  throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
  try {
​
    //K1  先根据 brokerName 得到 broker 地址信息,然后通过网络发送到指定的 Broker上。
    String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName)
      : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost());
​
    this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg,
                                                                      this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes());
​
​
  } catch (Exception e) {
​
    // IMP 原来消费者还会产生消息给 Broker 呀
    //K1   如果上述过程失败,则创建一条新的消息重新发送给 Broker,此时新消息的主题为重试主题:"%RETRY%" + ConsumeGroupName,
    //     注意,这里的主题和原先的消息主题没任何关系而是和消费组相关
    //     就是一个兜底策略,这个消费者组哪些消息没有消费掉,重新消费
    log.error("sendMessageBack Exception, " + this.defaultMQPushConsumer.getConsumerGroup(), e);
​
    Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody());
​
    String originMsgId = MessageAccessor.getOriginMessageId(msg);
    MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId);
​
    newMsg.setFlag(msg.getFlag());
    MessageAccessor.setProperties(newMsg, msg.getProperties());
​
    //IMP 真正的主题信息
    MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic());
​
    //重复消费次数+1
    MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1));
    MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes()));
    MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED);
    newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
​
    this.mQClientFactory.getDefaultMQProducer().send(newMsg);
  } finally {
    msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
  }
}
  • 上面的过程

    1. 获取 Broker 的地址,执行了【consumerSendMessageBack】,可以想想这个方法干了什么内容
    2. 如果发送失败了,此时会创建一个消息重新发送给 Broker,此时新消息的主题为重试主题:"%RETRY%" + ConsumeGroupName, 注意,这里的主题和原先的消息主题没任何关系而是和消费组相关,此时不会存在消息提,只需要 通过 msgId 就可以在 Broker 端获取到重新需要消费的消息
consumerSendMessageBack
public void consumerSendMessageBack(
  final String addr,
  final MessageExt msg,
  final String consumerGroup,
  final int delayLevel,
  final long timeoutMillis,
  final int maxConsumeRetryTimes
) throws RemotingException, MQBrokerException, InterruptedException {
  ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader();
​
  //按照偏移量重新获取数据 , CONSUMER_SEND_MSG_BACK
  RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader);
​
  requestHeader.setGroup(consumerGroup);
  requestHeader.setOriginTopic(msg.getTopic());
  requestHeader.setOffset(msg.getCommitLogOffset());
  requestHeader.setDelayLevel(delayLevel);
  requestHeader.setOriginMsgId(msg.getMsgId());
  requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);
​
  RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
                                                            request, timeoutMillis);
  assert response != null;
  switch (response.getCode()) {
    case ResponseCode.SUCCESS: {
      return;
    }
    default:
      break;
  }
​
  throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
}
  • 会构建一个请求【CONSUMER_SEND_MSG_BACK】,此请求中加了 msgId,那么可以通过 msgId 获取到消息

对应消费失败的消息,会向 Broker 重新发送消息,那么当前队列的偏移量是不是都应该增加,这样不会阻塞消息的拉取和消费,所以,不管成功还是失败,都需要将便宜量增加

/*
* K2 3 从处理队列的msgTreeMap中将消费成功以及消费失败但是发回broker成功的这批消息移除,然后返回msgTreeMap中的最小的偏移量
*     消息进度的更新,此处就知道了 TreeMap 的作用,为了保存最小的消费进度,为了更新消费进度
*
* IMP  不管成功与否都不会阻止 offset 的变大,如果消费失败了,会将消息以重试主题下的消息放入,然后进行消费
*   根据消费结果,设置ackIndex的值。
*   如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送 sendMessageBack,这里会创建新的消息(重试次数,延迟执行)。
*   更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。
*/
long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
//如果偏移量大于等于0并且处理队列没有被丢弃
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
  //尝试更新内存中的offsetTable中的最新偏移量信息,第三个参数是否仅单调增加offset为true
  this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}

具体 CONSUMER_SEND_MSG_BACK 的时候,broker 做了什么事儿呢,具体入口在

SendMessageProcessor

public CompletableFuture<RemotingCommand> asyncProcessRequest(ChannelHandlerContext ctx,
                                                              RemotingCommand request) throws RemotingCommandException {
  final SendMessageContext mqtraceContext;
  switch (request.getCode()) {
      // 消息失败的情况
    case RequestCode.CONSUMER_SEND_MSG_BACK:
      return this.asyncConsumerSendMsgBack(ctx, request);
    default:
      //省略无关的内容
      ....
  }
}
asyncConsumerSendMsgBack
  • 处理消费失败的 ack 请求,重新消费消息。目的就是之前说的,重新消费,不能阻塞其他消息拉取,消费端其实已经做了处理,不管成功与否,偏移量都增加,可想而知,Broker 端肯定是作为新的消息进行消费了,具体看看这个方法的逻辑就明白了
  • 又臭又长,直接上流程

    1. 根据消费组获取订阅信息

    2. 根据消费组获取新的主题,此主题和原始主题不一样,这也就可以想到的,这样就不会影响当前主题的消费了,消费进度就可以增加了,和 consumer 端保持一致.

    3. 通过偏移量获取到之前消费失败的消息信息

      1. 如果消费了太多次了,也不能阻塞重试队列的消费,不断的加到重试队列里面,一致失败,没有意义,所以太多次直接加入到死信队列中,方便后续通过人工干预进行处理,不得不说,各种处理都全面,也不会造成一致消费失败的消息

      2. 如果消息是,消费失败后的重新消费消息,那么会设置延迟级别信息 delayLevel = 3 + msgExt.getReconsumeTimes(); ,所以消费失败后的消息,不仅会重置其

        1. 新的主题为 RETRY_TOPIC + 消费组名称
        2. 设置延迟级别 DelayTimeLevel
    4. 通过新的主题,创建新的消息,然后加入到 commitLog 中。现在成功将消息发送到 commitlog 中,主题为 RETRY_TOPIC + 消费组名称,也就是消息重试的消息主题是基于消费组。而不是每一个主题都有一个重试主题。而是每一个消费组由一个重试主题。消息有几个重要点

      1. 新的主题为 RETRY_TOPIC + 消费组名称
      2. 设置了延迟级别 DelayTimeLevel
      3. 原始的真正的主题,记录在 properties 的 PROPERTY_RETRY_TOPIC
/**
* K1 处理消费失败的 ack 请求,重新消费消息
* @param ctx
* @param request
* @return
* @throws RemotingCommandException
*/
private CompletableFuture<RemotingCommand> asyncConsumerSendMsgBack(ChannelHandlerContext ctx,
                                                                    RemotingCommand request) throws RemotingCommandException {
  final RemotingCommand response = RemotingCommand.createResponseCommand(null);
  final ConsumerSendMsgBackRequestHeader requestHeader =
    (ConsumerSendMsgBackRequestHeader)request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class);
  String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getGroup());
  if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) {
    ConsumeMessageContext context = buildConsumeMessageContext(namespace, requestHeader, request);
    this.executeConsumeMessageHookAfter(context);
  }
​
​
  //K2 获取消费组的订阅信息
  SubscriptionGroupConfig subscriptionGroupConfig =
    this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup());
  if (null == subscriptionGroupConfig) {
    response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
    response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " "
                       + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
    return CompletableFuture.completedFuture(response);
  }
  if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())) {
    response.setCode(ResponseCode.NO_PERMISSION);
    response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending message is forbidden");
    return CompletableFuture.completedFuture(response);
  }
​
  if (subscriptionGroupConfig.getRetryQueueNums() <= 0) {
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
    return CompletableFuture.completedFuture(response);
  }
​
​
  //k2 根据重试主题创建或获取该主题的路由信息,此处的主题只和 消费者组有关系,所以是新的主题
  String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
  int queueIdInt = ThreadLocalRandom.current().nextInt(99999999) % subscriptionGroupConfig.getRetryQueueNums();
  int topicSysFlag = 0;
  if (requestHeader.isUnitMode()) {
    topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
  }
​
  //K3 主题配置信息
  TopicConfig topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
    newTopic,
    subscriptionGroupConfig.getRetryQueueNums(),
    PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
  if (null == topicConfig) {
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark("topic[" + newTopic + "] not exist");
    return CompletableFuture.completedFuture(response);
  }
​
  if (!PermName.isWriteable(topicConfig.getPerm())) {
    response.setCode(ResponseCode.NO_PERMISSION);
    response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic));
    return CompletableFuture.completedFuture(response);
  }
​
  //K2 通过偏移量 从 commitLog 中获取到需要重新消费的消息
  MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
  if (null == msgExt) {
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark("look message by offset failed, " + requestHeader.getOffset());
    return CompletableFuture.completedFuture(response);
  }
​
  // IMP  真正的主题
  final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
  if (null == retryTopic) {
    MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic());
  }
  msgExt.setWaitStoreMsgOK(false);
​
  int delayLevel = requestHeader.getDelayLevel();
​
  int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
  if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
    Integer times = requestHeader.getMaxReconsumeTimes();
    if (times != null) {
      maxReconsumeTimes = times;
    }
  }
​
​
  //k2 超了最大重试次数,或者延迟级别 不合规,则加入死信队列中
  if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
      || delayLevel < 0) {
​
    //IMP 死信队列,和消费组有关系
    newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
    queueIdInt = ThreadLocalRandom.current().nextInt(99999999) % DLQ_NUMS_PER_GROUP;
​
    topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
                                                                                                   DLQ_NUMS_PER_GROUP,
                                                                                                   PermName.PERM_WRITE | PermName.PERM_READ, 0);
​
    if (null == topicConfig) {
      response.setCode(ResponseCode.SYSTEM_ERROR);
      response.setRemark("topic[" + newTopic + "] not exist");
      return CompletableFuture.completedFuture(response);
    }
    msgExt.setDelayTimeLevel(0);
  } else {
    if (0 == delayLevel) {
      delayLevel = 3 + msgExt.getReconsumeTimes();
    }
    msgExt.setDelayTimeLevel(delayLevel);
  }
​
  MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
  msgInner.setTopic(newTopic);
  msgInner.setBody(msgExt.getBody());
  msgInner.setFlag(msgExt.getFlag());
  MessageAccessor.setProperties(msgInner, msgExt.getProperties());
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
  msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));
​
  msgInner.setQueueId(queueIdInt);
  msgInner.setSysFlag(msgExt.getSysFlag());
  msgInner.setBornTimestamp(msgExt.getBornTimestamp());
  msgInner.setBornHost(msgExt.getBornHost());
  msgInner.setStoreHost(msgExt.getStoreHost());
  msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);
​
  String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
  MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
​
​
  //K2 重新将该消息写入到 commitLog  中
  // IMP 成功将消息发送到 commitlog 中,主题为  RETRY_TOPIC  +  消费组名称,,
  //     也就是消息重试的消息主题是基于消费组。而不是每一个主题都有一个重试主题。
  //     而是每一个消费组由一个重试主题。那这些主题的消息,又是如何在被消费者获取并进行消费的。
  CompletableFuture<PutMessageResult> putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
  return putMessageResult.thenApply((r) -> {
    if (r != null) {
      switch (r.getPutMessageStatus()) {
​
          //K2  保存成功
        case PUT_OK:
          String backTopic = msgExt.getTopic();
          String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
          if (correctTopic != null) {
            backTopic = correctTopic;
          }
          if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(msgInner.getTopic())) {
            this.brokerController.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic());
            this.brokerController.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(), r.getAppendMessageResult().getWroteBytes());
            this.brokerController.getBrokerStatsManager().incQueuePutNums(msgInner.getTopic(), msgInner.getQueueId());
            this.brokerController.getBrokerStatsManager().incQueuePutSize(msgInner.getTopic(), msgInner.getQueueId(), r.getAppendMessageResult().getWroteBytes());
          }
          this.brokerController.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic);
          response.setCode(ResponseCode.SUCCESS);
          response.setRemark(null);
          return response;
        default:
          break;
      }
      response.setCode(ResponseCode.SYSTEM_ERROR);
      response.setRemark(r.getPutMessageStatus().name());
      return response;
    }
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark("putMessageResult is null");
    return response;
  });
}

目前,重试机制的前半部分已经讲解完成,再次复习一下:

  1. 根据消费结果,设置ackIndex的值。
  2. 如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送 sendMessageBack,这里会创建新的消息(重试次数,延迟执行)。
  3. 更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。

消息现在是存储到 commitlog 文件里了,那怎么消费呢。

继续看 asyncPutMessage方法,最终定位到了 CommitLog#asyncPutMessage 方法中

public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
  // Set the storage time
  //设置存储时间
  msg.setStoreTimestamp(System.currentTimeMillis());
​
  // Set the message body BODY CRC (consider the most appropriate setting
  // on the client)
  //设置消息正文CRC
  msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
  // Back to Results
  AppendMessageResult result = null;
​
  StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
​
  String topic = msg.getTopic();
  //        int queueId msg.getQueueId();
​
  /*
  * K2 1 处理延迟消息的逻辑
  *
  * 替换topic和queueId,保存真实topic和queueId
  */
  //根据sysFlag获取事务状态,普通消息的sysFlag为0
  final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
​
  //如果不是事务消息,或者commit提交事务小i
  if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
      || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
​
    //IMP 延迟消息
    // Delay Delivery
    //获取延迟级别,判断是否是延迟消息
    if (msg.getDelayTimeLevel() > 0) {
​
      //如果延迟级别大于最大级别,则设置为最大级别
      if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
        msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
      }
​
      //获取延迟队列的topic,固定为 SCHEDULE_TOPIC_XXXX
      topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
      //根据延迟等级获取对应的延迟队列id, id = level - 1
      int queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
​
      // Backup real topic, queueId
      //使用扩展属性REAL_TOPIC 记录真实topic,此时的主题,进入到钱的主题,
      MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
      //使用扩展属性REAL_QID 记录真实queueId
      MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
      msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
      //更改topic和queueId为延迟队列的topic和queueId
      msg.setTopic(topic);
      msg.setQueueId(queueId);
    }
  }
  //...省略无关的内容...
}

最后消息变成了,延迟消息,此时消息存了 3 种主题信息

  1. REAL_TOPIC 为 RETRY_TOPIC + 消费组名称
  2. Topic 为 SCHEDULE_TOPIC_XXXX
  3. PROPERTY_RETRY_TOPIC 为 消息失败之前的真正主题

得了,最后按照延迟消费的消息进行处理了,延迟消息消费的逻辑,后面学习,对于上面的消息重试,进行一波总结

1、如果返回结果是 CONSUME_SUCCESS,此时 ackIndex = msg.size() - 1, 再看发送 sendMessageBack 循环的条件,for (int i = ackIndex + 1; i < msg.size() ;;) 从这里可以看出如果消息成功,则无需发送 sendMsgBack 给 broker;如果返回结果是RECONSUME_LATER, 此时 ackIndex = -1 ,则这批所有的消息都会发送消息给 Broker,也就是这一批消息都得重新消费。

如果发送ack消息失败,则会延迟5s后重新在消费端重新消费。

首先消费者向 Broker 发送 ACK 消息,如果发生成功,重试机制由 broker 处理,如果发送 ack 消息失败,则将该任务直接在消费者这边,再次将本次消费任务,默认演出5S后在消费者重新消费。

根据消费结果,设置ackIndex的值。 如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送sendMessageBack。 更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。 2、需要延迟执行的消息,在存入 commitlog 之前,会备份原先的主题(retry+消费组名称)、与消费队列ID,然后将主题修改为SCHEDULE_TOPIC_XXXX,会被延迟任务 ScheduleMessageService 延迟拉取。

3、ScheduleMessageService 在执行过程中,会再次存入 commitlog 文件中放入之前,会清空延迟等级,并恢复主题与队列,这样,就能被消费者所消费,因为消费者在启动时就订阅了该消费组的重试主题。

总结

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

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

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

遗留问题

对于消费端

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

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