RocketMq-Consumer

producer源码结构如下:

我们通常使用mq接受消息,实例化consumer的方式就是:

DefaultMQPushConsumer consumer =  new  DefaultMQPushConsumer( "MyTopic-Consume-Single" );
                                                        //实际调用了
public  DefaultMQPushConsumer(String consumerGroup) {
         this (consumerGroup, (RPCHook) null new  AllocateMessageQueueAveragely());     //注意此处默认创建了一个消费负载均衡策略
}


所以我们就从DefaultMQPushConsumer开始说起吧。

DefaultMQPushConsumer继承了ClientConfig并且实现了MQPushConsumer接口。与producer类似,同时注入一个重要的属性

protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;

public  class  DefaultMQPushConsumer  extends  ClientConfig  implements  MQPushConsumer {
 
     protected  final  transient  DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;


MQPushConsumer接口定义了一些最基本的方法,例如:

void  registerMessageListener( final  MessageListenerConcurrently messageListener);             //消费者注册监听器。
void  subscribe( final  String topic,  final  String subExpression)  throws  MQClientException;          //设置订阅的topic以及tag的方法。


ClientConfig在producer里已经介绍过了,就不重复说了。

而消费消息大致流程如下,我们具体看看各个步骤都做了什么。

consumer.setNamesrvAddr( "10.3.254.52:9876" );                                //设置namesrv,实际是调用ClientConfig.setNamesrvAddr(String namesrvAddr);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);   //设置消费起始位置。
consumer.setConsumeMessageBatchMaxSize( 5 );                                  //设置每次消费消息数量。
consumer.subscribe( "TopicTest" "*" );                                       //设置订阅的topic和tag。
consumer.registerMessageListener( new  StockListener() );                     //注册消费端的监听器,分为普通消费监听器和顺序消费监听器。其实就是把StockListener赋值给DefaultMQPushConsumer的MessageListener属性。
consumer.start();                                                           //启动消费者。

重点看一下,启动消费者都干了些什么?

   consumer.start();该方法实际调用了DefaultMQPushConsumerImpl .start()     【以下环节都是针对普通消费顺序】

  1、首先进行状态检查,如果是CREATE_JUST才进行以下操作:

  2、将状态置为START_FAILED。

  3、this.checkConfig();进行参数检查,检查范围比较多,大致如下:

       3.1)consumerGroup不能为空,不能含有非法字符,长度不能超过255。

       3.2) 不能为默认的消费者组名"DEFAULT_CONSUMER"。

       3.3)消费模型不能为空,即必须是集群消费或者广播消费的一种【默认是集群消费】

       3.4)起始消费位置不能为空。【只对初次消费有效】

       3.5)消费时间戳不能为空 -- (消息回溯,默认默认值是半小时以前)【只对初次消费有效】

       3.6)消费负载均衡策略不能为空,默认是AllocateMessageQueueAveragely。下面我们看看,这个策略是怎么做到消费负载均衡的。

               public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) ;这个方法就是做消费负载均衡的。他的参数currentCID代表这个消费者组的其中一个消费者                                 id,List<MessageQueue> mqAll代表所有的订阅的topic下所有的messageQueue, List<String> cidAll代表消费者组的消费者集合。该方法最终会为每个消费者分配对应的messageQueue。具体算法查看源码。例如,一个8个                                     messageQueue,消费者组内有三个消费者。

               最终分配情况大致如下:

                            sonsumer0          consumer1          consumer2

                              queue0                queue3                 queue6

                              queue1                queue4                 queue7

                              queue2                queue5

        3.7)存储订阅关系的subscription不能为空,private Map<String /* topic */, String /* sub expression */subscription new HashMap<String, String>();

        3.8)消费端注册的监听器不能为空,并检查是普通消费还是顺序消费,并且必须是这二者其一。

        3.9)检查消费者默认线程池最小和最大是否是在1~1000且最小值不能大于最大值。【默认最小20,最大64】每1分钟调整一次线程池,这也是针对消费者来说的,具体为如果消息堆积超过10W条,则调大线程池,最多64个线程;如果消      息堆积少于8W条,则调小线程池。

        3.10)检查单队列并行消费最大跨度consumeConcurrentlyMaxSpan,不能小于1不能大于65535。consumeConcurrentlyMaxSpan这个值默认是2000,当RocketMQ发现本地缓存的消息的最大值-最小值差距大于这个值(2000)的时候,会  触发流控——也就是说如果头尾都卡住了部分消息,达到了这个阈值就不再拉取消息。

        3.11)检查拉消息本地队列缓存消息最大数pullThresholdForQueue,不能小于1,大于65535。【默认是1000】。含义是:消费者不间断的从broker拉取消息,消息拉取到本地队列,然后本地消费线程消费本地消息队列,只是一个异步过    程,拉取线程不会等待本地消费线程,这种模式实时性非常高(本地消息队列达到解耦的效果,响应时间减少)。对消费者对本地队列有一个保护,因此本地消息队列不能无限大,否则可能会占用大量内存。ps:还记得broker启动至少需要4G的磁盘吗?还记得每条消息的最大值默认是4M吗?那这里设置的1000是巧合呢还是有意为之?

        3.12)检查pullThresholdForTopic值是否为默认的-1,如果不是则必须在1~65535之间。【表示每个topic在本地缓存最多的消息条数】

        3.13)检查消息缓存值pullThresholdSizeForQueue,不能小于1M,不能大于1024M。【默认是100M】

        3.14)检查每次批量消费规模consumeMessageBatchMaxSize,不能小于1条,不能大于1024条。【默认是1条】

        3.15)检查每次从broker批量拉取消息数量pullBatchSize,不能小于1条,不能大于1024条。【默认32条】

   4、调用this.copySubscription();Client端信号收集,拷贝订阅信息,将消费者的topic订阅关系设置到rebalanceImpl的SubscriptionInner的map中用于负载。

   5、如果是集群消费模式,则将客户端实例名由"DEFAULT"变成客户端实例的进程号。

   6、调用MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumerthis.rpcHook);方法,以当前consumer作为参数实例化一个消费端实例。

   7、接着完善rebalanceImpl实例,给他设置消费者组,消费模型,消费端负载均衡策略,以及消费端实例。

   8、构建PullAPIWrapper对象,该对象封装了具体拉取消息的逻辑,PULL,PUSH模式最终都会调用PullAPIWrapper类的方法从Broker拉取消息。

   9、this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);又是什么钩子,感觉可有可无,因为跟进去发现接口定义的方法没被实现。

10 this .offsetStore =  this .defaultMQPushConsumer.getOffsetStore();给offsetStore赋值,该类主要定义了,对于消费进度管理的一些方法。如果没有,则新建一个。
            若是广播消费: this .offsetStore =  new  LocalFileOffsetStore( this .mQClientFactory,  this .defaultMQPushConsumer.getConsumerGroup());
            若是集群消费: this .offsetStore =  new  RemoteBrokerOffsetStore( this .mQClientFactory,  this .defaultMQPushConsumer.getConsumerGroup());
            同时把新建的offsetStore赋值给defaultMQPushConsumer的offsetStore。

    11、this.offsetStore.load();加载消费进度,集群和广播是不一样的!

    12、建立消费线程,普通消费和顺序消费不同!

顺序消费: this .consumeMessageService = new  ConsumeMessageOrderlyService( this , (MessageListenerOrderly)  this .getMessageListenerInner());
普通消费: this .consumeMessageService =  new  ConsumeMessageConcurrentlyService( this , (MessageListenerConcurrently)  this .getMessageListenerInner());


    13、this.consumeMessageService.start();介绍这个方法之前首先说一下rocketMq的ACK机制。RocketMQ是以consumer group+queue为单位是管理消费进度的,以一个consumer offset标记这个这个消费组在这条queue上的消费进度。每次消  息成功后,本地的消费进度会被更新,然后由定时器定时同步到broker,以此持久化消费进度。但是每次记录消费进度的时候,只会把一批消息中最小的offset值为消费进度值。这钟方式和传统的一条message单独ack的方式有本质的区 别。性能上提升的同时,会带来一个潜在的重复问题——由于消费进度只是记录了一个下标,就可能出现拉取了100条消息如 2101-2200的消息,后面99条都消费结束了,只有2101消费一直没有结束的情况。在这种情况下,RocketMQ 为了保证消息肯定被消费成功,消费进度职能维持在2101,直到2101也消费结束了,本地的消费进度才会一下子更新到2200。在这种设计下,就有消费大量重复的风险。如2101在还没有消费完成的时候消费实例突然退出(机器断电,或 者被kill)。这条queue的消费进度还是维持在2101,当queue重新分配给新的实例的时候,新的实例从broker上拿到的消费进度还是维持在2101,这时候就会又从2101开始消费,2102-2200这批消息实际上已经被消费过还是会投递一次。

            毫无疑问,这个方法就是为了解决这个问题(ACK卡进度)。跟踪进去,发现启动了一个定时线程,15分钟一个周期。而运行的方法叫做cleanExpireMsg();顾名思义,清理过期消息。再联系我们说的ACK卡进度问题,就不难猜出这个方  法就是不让一直未消费成功的消息卡住整体的消费进度。继续跟入代码。Iterator<Map.Entry<MessageQueue, ProcessQueue>> it =this.defaultMQPushConsumerImpl.getRebalanceImpl().getProcessQueueTable().entrySet().iterator(); //得到        该消费者的消费的breoker的队列集合,已经对应本地处理进度的快照【Queue consumption snapshot】,对于普通消费,该线程会检查本地缓存的消息里的第一条消息,看与当前时间相差是否超过15分钟,若不是,则跳过,该方法结束。若是则表明该消息超过超时时间还没有被消费成功,那么首先会给broker回发消息,表明该消息消费失败,让broker稍后再投递【此处建议细看原代码】。然后本地缓存里删除这条消息。这种删除,一个周期内最多删除16条超时消息。什么意 思?举个例子,假设我拉取的2101-2200,其中2201-2217都超时没消费成功,那么卡进度问题,至少要30分钟才会得到解决,也就是两轮cleanExpireMsg()的调度!

    14、将DefaultMQPushConsumerImpl注册到客户端实例中,也就是往ConcurrentMap<String/* group */, MQConsumerInner> consumerTable  中put该consumer。

            若未能成功注册,this.serviceState = ServiceState.CREATE_JUST;//状态置为可启动状态        this.consumeMessageService.shutdown();//停掉清理过期消息的线程。

    15、mQClientFactory.start();客户端实例启动,这里和producer客户端实例启动执行的是一模一样的代码。再啰嗦说一遍:

15.1 this .serviceState = ServiceState.START_FAILED;    以免客户端同一个进程中重复启动;
15.2 this .mQClientAPIImpl.start();   启动客户端netty与服务端建立长连接。
15.3 this .startScheduledTask();启动各种任务调度  
        15.3 . 1 )从NameSrv遍历TopicRouteInfo(Topic的路由信息有brokerName,queueId组成),然后更新producer和consumer的topic信息     【 30 秒一次】
        15.3 . 2 ) 清理离线的broker        【 30 秒一次】
        15.3 . 3 )向所有在MQClientInstance.brokerAddrTable列表中的Broker发送心跳消息    【 30 秒一次】
        15.3 . 4 )持久化consumer消费进度    【 5 秒一次】
        15.3 . 5 )启动线程池线程数调整线程。   【每分钟调整一次】
        15.3 . 6 this .serviceState = ServiceState.RUNNING;   设置DefaultMQProducerImpl的ServiceState为RUNNING,使producer避免重复启动;
  15.4 this .pullMessageService.start();  启动拉消息服务PullMessageService。
  15.5 this .rebalanceService.start();  启动消费端负载均衡服务RebalanceService     //此时该线程并没有运行,等待唤醒。
  15.6 this .defaultMQProducer.getDefaultMQProducerImpl().start( false );        //这个默认的producer就是为了给broker回发消息!!!
  15.7 this .serviceState = ServiceState.RUNNING;       设置DefaultMQProducerImpl的ServiceState为RUNNING,使producer避免重复启动;

   

16 this .updateTopicSubscribeInfoWhenSubscriptionChanged();     //从namesrv更新topic的路由信息【建议详细阅读源码】。
17 this .mQClientFactory.checkClientInBroker();                 //具体操作就是使用netty给broker发送请求,检查broker状态。
18 this .mQClientFactory.sendHeartbeatToAllBrokerWithLock();    //向所有在MQClientInstance.brokerAddrTable列表中的Broker发送心跳消息
19 this .mQClientFactory.rebalanceImmediately();                //唤醒负载均衡线程。


    到这里就是消费者启动的完全过程了,但大致如下图:


有两个地方我们需要回头仔细看一下: 15.3)启动拉消息服务PullMessageService。做了什么?     19、 唤醒负载均衡线程。做了什么?


    20、PullMessageService里有一个LinkedBlockingQueue<PullRequest> pullRequestQueue,这里面存储这要从broker拉取消息的请求信息,只要这个线程一直运行着,就会不停的从里面取出请求信息,然后调用this.pullMessage(pullRequest);根据请求里consumerGroup获取对应的DefaultMQPushConsumerImpl,在调用DefaultMQPushConsumerImpl的pullMessage(pullRequest);开始真正的从broker拉取消息:

   1)pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());  给请求里的ProcessQueue设置上次拉取消息的时间。

   2)this.makeSureStateOK();   确认consumer的状态是否为 RUNNING。

   3)检查consumer的pause属性,若是true,代表暂停,则建立一个调度线程每秒触发一次尝试拉取消息,this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);

   4)long cachedMessageCount = processQueue.getMsgCount().get();   获取本地缓存的还未消费成功的消息条数。

        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 1024);   获取本地缓存的还未消费成功的消息的总大小,以M为单位。

   5)若本地缓存的还未消费成功的消息条数>1000(有没有回忆起来,之前说过这个值)或者本地缓存的还未消费成功的消息的总大小>100M,代表达到流控阈值,则建立一个调度线程,每50毫秒再次尝试拉取消息。这里个人觉得,要注意这类情况下roketMq本身的日志记录:

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)。也就是说,如果我们发现消费端一直没有新的消息,而实际有足够的消息。我觉得就可以检查日志,查找上述关键字,看是否因为本地原因,触发流控了。

   6)如果不是顺序消费,本地未消费消息的跨度是不能大于2000的,例如,目前我就只有两条消息没消费成功,分别是1103,3304,即便只有两条,内存几乎可以忽略不计,这时候也不会再拉取新的消息。会建立一个调度线程每50毫秒再次尝试拉取消息。那么这个状态什么时候会改变呢。两种情况,消费端成功消费了消息或者consumeMessageService到了周期,执行过期消息清理(回忆起来了没,没回忆起来,就看13、)。

        如果是顺序消费,.............................................................................................待补充

   7)final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());     获取请求消息下topic对应的订阅信息。这些东西是在7、处塞进去的。

   8)匿名类实现拉取消息的回调接口,采用异步方式拉取消息时,在收到Broker的响应消息之后,回调该方法执行业务调用者的回调逻辑。PullCallback pullCallback = new PullCallback() {

................}      一个void onSuccess(final PullResult pullResult)方法       一个void onException(final Throwable e)方法

先说onSuccess(final PullResult pullResult)方法  :

        8.1)调用pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,subscriptionData)方法处理broker返回的结果。

        8.1.1)调用PullAPIWrapper.updatePullFromWhichNode(MessageQueue mq, long brokerId)方法用Broker返回的PullResultExt.suggestWhichBrokerId变量值更新PullAPIWrapper.pullFromWhichNodeTable:ConcurrentHashMap <MessageQueue,AtomicLong/* brokerId */>变量中当前拉取消息PullRequest.messageQueue对象对应的BrokerId。若以messageQueue为key值从pullFromWhichNodeTable中获取的BrokerId为空则将PullResultExt.suggestWhichBrokerId存入该列表中,否则更新该MessageQueue对应的value值为suggestWhichBrokerId;          //这里其实就是说broker根据自身状态,建议从主broker还是从从broker拉取消息!

              8.1.2)若pullResult.status=FOUND,则继续下面的处理逻辑,否则设置PullResultExt.messageBinary=null并返回该PullResult对象;

              8.1.3)对PullResultExt.messageBinary变量进行解码,得到MessageExt列表。(这就是我们拉取的消息)

              8.1.4)只保留我们订阅tag集合中的MessageExt对象,构成新的MessageExt列表,取名msgListFilterAgain。

              8.1.5)给其中的每一个消息设置broker返回的MIN_OFFSET和MAX_OFFSET属性。

              8.1.6)返回pullRequst。

如果pullResult.getPullStatus()=FOUND;


          8.2)该PullRequest对象的nextOffset变量值表示本次消费的开始偏移量,赋值给临时变量prevRequestOffset;

          8.3)取PullResult.nextBeginOffset的值(Broker返回的下一次消费进度的偏移值)赋值给PullRequest.nextOffset变量值;

          8.4)记录一下该次的拉取消耗时间。

         8.5)若PullResult.MsgFoundList列表为空,则调用DefaultMQPushConsumerImpl.executePullRequestImmediately(PullRequest pullRequest)方法将该拉取请求对象PullRequest重新延迟放入PullMessageService线程的pullRequestQueue队列中,然后跳出该onSucess方法;否则继续下面的逻辑;

          8.6)firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();   获取第一个消息消费进度。

          8.7)记录一下该消费者在此topic下此次拉取消息的数量。

          8.8)processQueue.putMessage(pullResult.getMsgFoundList())   将MessageExt列表存入ProcessQueue.msgTreeMap:TreeMap<Long, MessageExt>变量中,放入此变量的目的是:第一在顺序消费时从该变量列表中取消息进行消费,第二可以用此变量中的消息做流控;大致逻辑如下:

               A)遍历List<MessageExt>列表,以每个MessageExt对象的queueOffset值为key值,将MessageExt对象存入msgTreeMap:TreeMap<Long, MessageExt>变量中;该变量类型根据key值大小排序;

               B)更新ProcessQueue.msgCount变量,记录消息个数;

              C)经过第A步处理之后,若msgTreeMap变量不是空并且ProcessQueue.consuming为false(初始化为false)则置consuming为true(在该msgTreeMap变量消费完之后再置为false)、置临时变量dispatchToConsume为true;否则置临时变量dispatchToConsume为false表示没有待消费的消息或者msgTreeMap变量中存入了数据还未消费完,在没有消费完之前不允许在此提交消费请求,在消费完msgTreeMap之后置consuming为false;

              D)取List<MessageExt>列表的最后一个MessageExt对象,该对象的properties属性中取MAX_OFFSET的值,减去该MessageExt对象的queueOffset值,即为Broker端该topic和queueId下消息队列中未消费的逻辑队列的大小,将该值存入ProcessQueue.msgAccCnt变量,用于MQClientInstance. adjustThreadPool()方法动态调整线程池大小(在MQClientInstance中启动定时任务定时调整线程池大小);

              E)返回临时变量dispatchToConsume值;                //此处的msgTreeMap很重要,流控就依赖他的一些属性

          8.9)调用ConsumeMessageService.submitConsumeRequest(List <MessageExt> msgs,ProcessQueue processQueue,MessageQueue messageQueue, boolean dispathToConsume)方法,其中dispathToConsume的值由上一步所得,在顺序消费时使用,为true表示可以消费;大致逻辑如下:

              A)若是 顺序消费 ,则调用ConsumeMessageOrderlyService. submitConsumeRequest(List<MessageExt> msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispathToConsume)方法;在该方法中,若上次的msgTreeMap变量中的数据还未消费完(即在2.4步中返回dispathToConsume=false)则不执行任何逻辑;若dispathToConsume=true(即上一次已经消费完了)则以ProcessQueue和MessageQueue对象为参数初始化ConsumeMessageOrderlyService类的内部线程类ConsumeRequest;然后将该线程类放入ConsumeMessageOrderlyService.consumeExecutor线程池中。从而可以看出顺序消费是从ProcessQueue对象的TreeMap树形列表中取消息的。

             B)若是 并发消费 ,则调用ConsumeMessageConcurrentlyService.submitConsumeRequest(List<MessageExt> msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume)方法;在该方法中,则根据批次中最大信息条数(由DefaultMQPushConsumer.consumeMessageBatchMaxSize设置,默认为1)来决定是否分提交到线程池中,大致逻辑为:首先比较List<MessageExt>列表的个数是否大于了批处理的最大条数,若没有则以该List<MessageExt>队列、ProcessQueue对象、MessageQueue对象初始化ConsumeMessageConcurrentlyService的内部类 ConsumeRequest 的对象,并放入ConsumeMessageConcurrentlyService.consumeExecutor线程池中;否则遍历List<MessageExt>列表,每次从List<MessageExt>列表中取consumeMessageBatchMaxSize个MessageExt对象构成新的List<MessageExt>列表,然后以新的MessageExt队列、ProcessQueue对象、MessageQueue对象初始化ConsumeMessageConcurrentlyService的内部类 ConsumeRequest 的对象并放入ConsumeMessageConcurrentlyService.consumeExecutor线程池中,直到该队列遍历完为止。从而可以看出并发消费是将从Broker获取的MessageExt消息列表分配到各个 ConsumeRequest 线程中进行并发消费。 

            8.10)检查拉取消息的间隔时间(DefaultMQPushConsumer.pullInterval,默认为0),若大于0,则调用DefaultMQPushConsumerImpl. executePullRequestLater方法,在间隔时间之后再将PullRequest对象放入PullMessageService线程的pullRequestQueue队列中;若等于0(表示立即再次进行拉取消息),则调用DefaultMQPushConsumerImpl. executePullRequestImmediately方法立即继续下一次拉取消息,从而形成一个循环不间断地拉取消息的过程;

如果pullResult.getPullStatus()=NO_NEW_MSG或者NO_MATCHED_MSG:

       取PullResult.nextBeginOffset的值(Broker返回的下一次消费进度的偏移值)赋值给PullRequest.nextOffset变量值;

      更新消费进度offset。调用DefaultMQPushConsumerImpl.correctTagsOffset(PullRequest pullRequest)方法。若没有获取到消息(即ProcessQueue.msgCount等于0)则更新消息进度。对于LocalFileOffsetStore或RemoteBrokerOffsetStore类,均调用updateOffset(MessageQueue mq, long offset, boolean increaseOnly)方法,而且方法逻辑是一样的,以MessageQueue对象为key值从offsetTable:ConcurrentHashMap<MessageQueue, AtomicLong>变量中获取values值,若该values值为空,则将MessageQueue对象以及offset值(在3.1步中获取的PullResult.nextBeginOffset值)存入offsetTable变量中,若不为空,则比较已经存在的值,若大于已存在的值才更新; 

       调用DefaultMQPushConsumerImpl.executePullRequestImmediately方法立即继续下一次拉取。

如果pullResult.getPullStatus()=OFFSET_ILLEGAL

      取PullResult.nextBeginOffset的值(Broker返回的下一次消费进度的偏移值)赋值给PullRequest.nextOffset变量值;

      设置PullRequest.processQueue.dropped等于true,将此该拉取请求作废;

      创建一个匿名Runnable线程类,然后调用DefaultMQPushConsumerImpl.executeTaskLater(Runnable r, long timeDelay)方法将该线程类放入PullMessageService.scheduledExecutorService: ScheduledExecutorService调度线程池中,在10秒钟之后(可能有地方正在使用,避免受到影响)执行该匿名线程类;该匿名线程类的run方法逻辑如下:

        A)调用OffsetStore.updateOffset(MessageQueue mq, long offset, boolean increaseOnly)方法更新更新消费进度offset;

       B)调用OffsetStore.persist(MessageQueue mq)方法:对于广播模式下offsetStore初始化为LocalFileOffsetStore对象,该对象的persist方法没有处理逻辑;对于集群模式下offsetStore初始化为RemoteBrokerOffsetStore对象,该对象的persist方法中,首先以入参MessageQueue对象为key值从RemoteBrokerOffsetStore.offsetTable: ConcurrentHashMap<MessageQueue, AtomicLong>变量中获取偏移量offset值,然后调用updateConsumeOffsetToBroker(MessageQueue mq, long offset)方法向Broker发送UPDATE_CONSUMER_OFFSET请求码的消费进度信息; 
       C)以PullRequest对象的messageQueue变量为参数调用RebalanceImpl.removeProcessQueue(MessageQueue mq)方法,在该方法中,首先从RebalanceImpl.processQueueTable: ConcurrentHashMap<MessageQueue, ProcessQueue>变量中删除MessageQueue记录并返回对应的ProcessQueue对象;然后该ProcessQueue对象的dropped变量设置为ture;最后以MessageQueue对象和ProcessQueue对象为参数调用removeUnnecessaryMessageQueue方法删除未使用的消息队列的消费进度。【主要是删除远程broker的未使用的消息队列的消费进度】

onException方法

延迟提交拉取消息请求,3秒钟之后再将该PullRequest请求重新放入PullMessageService线程的pullRequestQueue队列中;


   9)集群模式下计算提交的消费进度。

  10)计算请求的 订阅表达式 和 是否进行filtersrv过滤消息

  11)计算拉取消息系统标识(一些与运算,暂时不知道什么用)

 

12 ) this .pullAPIWrapper.pullKernelImpl(                   // 执行拉取。下面对参数做一下说明
         pullRequest.getMessageQueue(),                    //拉取消息的目标的queue【包含topic,brokername,brokerid】
         subExpression,                                    //订阅的tag
         subscriptionData.getExpressionType(),
         subscriptionData.getSubVersion(),                 //订阅的版本号
         pullRequest.getNextOffset(),                      //拉取队列开始位置
         this .defaultMQPushConsumer.getPullBatchSize(),    //每次拉取的数量
         sysFlag,                                          //拉取消息系统标识
         commitOffsetValue,                                //提交消费进度
         BROKER_SUSPEND_MAX_TIME_MILLIS,                   //broker挂起请求最大时间
         CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,             //请求broker超时时长
         CommunicationMode.ASYNC,                          //拉取模式为异步
         pullCallback                                      //拉取回调
);

如果拉取请求发生异常时,提交延迟拉取消息请求。

this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);


看一下this.pullAPIWrapper.pullKernelImpl实际上是怎么执行消息的拉取工作的:

       获取broker信息,主要包含brokerAddr,是否是从broker,以及broker的版本号,

FindBrokerResult findBrokerResult =  this .mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), this .recalculatePullFromWhichNode(mq),  false );  //this.recalculatePullFromWhichNode(mq)是获取推荐brokerId的。


       如果null 
== findBrokerResult,此时需要从nameServer更新topic路由信息,再次调用

this .mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), this .recalculatePullFromWhichNode(mq),  false );

若此时findBrokerResult仍然为null,则抛出broker不存在的异常。

如果findBrokerResult不为null  :  检查版本号,如果版本号<4.1.0同时expressionType(过滤表达式)!=tag则抛出未升级到支持文件消息的版本,新版本除了tag过滤消息,还可以用sql过滤。

构建请求头:

PullMessageRequestHeader requestHeader =  new  PullMessageRequestHeader();
requestHeader.setConsumerGroup( this .consumerGroup);                    //塞入消费者组
requestHeader.setTopic(mq.getTopic());                                 //塞入订阅的topic
requestHeader.setQueueId(mq.getQueueId());                             //塞入订阅的broker的queueID
requestHeader.setQueueOffset(offset);                                  //本次拉取消息的位置
requestHeader.setMaxMsgNums(maxNums);                                  //本次拉取消息的数量
requestHeader.setSysFlag(sysFlagInner);                               // 应该是用来判断是否使用文件过滤器
requestHeader.setCommitOffset(commitOffset);                          //消费者提交的消费进度
requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);    //拉取消息请求被挂起的超时时间
requestHeader.setSubscription(subExpression);                         //订阅的tag          
requestHeader.setSubVersion(subVersion);                              // 一个时间戳,某个消费者每更新了订阅信息,就会新生成一个
requestHeader.setExpressionType(expressionType);                      //过滤表达式
                                                                      //然后判断是否使用了文件过滤,如果是的话,那么broker的地址应该就是对应的文件服务器的地址。
if  (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
     brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
}
//接着调用
PullResult pullResult =  this .mQClientFactory.getMQClientAPIImpl().pullMessage(brokerAddr,
requestHeader,
timeoutMillis,
communicationMode,
pullCallback);
//进而调用
public  PullResult pullMessage();
//我们拉取消息是采用异步的,所以调用
this .pullMessageAsync(addr, request, timeoutMillis, pullCallback);
//进而调用netty客户端发拉取取消息的请求:
this .remotingClient.invokeAsync(addr, request, timeoutMillis,  new  InvokeCallback(){.........}); //   这里采用匿名内部内创建一个监听器,用于接受消息拉取成功后的回调信息。
//然后成功收到broker的返回信息就调用
pullCallback.onSuccess(pullResult);
//否则调用
pullCallback.onException(); 
后续在producer发消息中就讲过了,netty的一下东西。

拉取消息流程如下图:



     到此,我们解决了回头看的两个疑问中的15.3( 到这里就是消费者启动的完全过程了,有两个地方我们需要回头仔细看一下: 15.3)启动拉消息服务PullMessageService。做了什么?     19、 唤醒负载均衡线程。做了什么?),接下来,我们看看在DefaultMQPushConsumer启动时唤醒的负载均衡线程,做了什么:


     当唤醒rebalanceImpl时,在集群消费模式下,他会执行如下操作:

首先通过topic获取订阅的queue的set集合、然后通过消费者组名和topic给broker发送请求,获取该消费者组下的消费者id集合。

Set<MessageQueue> mqSet =  this .topicSubscribeInfoTable.get(topic);
List<String> cidAll =  this .mQClientFactory.findConsumerIdList(topic, consumerGroup);


      将这两个集合排序,然后将他们作为参数调用此方法,做负载均衡

List<MessageQueue> allocateResultallocateResult = strategy.allocate( this .consumerGroup, this .mQClientFactory.getClientId(),mqAll,cidAll);

说白了就是,我告诉你我这个消费者组有几个消费者,我要订阅的topic下的所有的broker的queue的集合,你给我用指定策略分配一下,看看我的当前这个消费者应该订阅那几个queue。

//然后调用
updateProcessQueueTableInRebalance( final  String topic,  final  Set<MessageQueue> mqSet,  final  boolean  isOrder);
//在负载均衡线程内
ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable  记录了每个订阅的queue与本地对应的处理进度。
如果发现传入的mqSet不包含它本身记录的MessageQueue,则说明他之前记录的要订阅的queue在负载均衡之后不再订阅了,
则在本地和broker同时持久化对应MessageQueue的消费进度,在本地和broker删除对应MessageQueue的消费进度。
然后将rebalanceImpl中processQueueTable对应的记录删除。而且如果发现本地消息缓存的ProcessQueue已经超过 2 分钟没有拉取消息,
即使负载均衡后仍然包含该MessageQueue,也做不包含该MessageQueue的一样的处理。


         然后调用
computePullFromWhere计算从broker拉取消息的位置,拿我们常用的CONSUME_FROM_FIRST_OFFSET 来说:会调用long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);得到消费进度。

由于通常消费进度每隔一段时间就会持久化到文件里,所以一般情况下,我们也是存文件中获取消费进度,而不是从内存。然后ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);插入一条新的记录。

构造拉取请求:

PullRequest pullRequest =  new  PullRequest();
pullRequest.setConsumerGroup(consumerGroup);     //设置消费者组
pullRequest.setNextOffset(nextOffset);      //设置拉取位置
pullRequest.setMessageQueue(mq);        //设置从哪个queue拉取消息
pullRequest.setProcessQueue(pq);         //设置本地对应存的queue
pullRequestList.add(pullRequest);         //添加请求
然后将这些请求逐个放入PullMessageService 的  private  final  LinkedBlockingQueue<PullRequest> pullRequestQueue 中。然后...请看 20


到此我们的负载均衡后,订阅队列改变后,还要通知broker:
public void messageQueueChanged(String topic, Set<MessageQueue> mqAll, Set<MessageQueue> mqDivided) {..........}

在此方法中1、取当前时间赋值给subscriptionData.setSubVersion(newVersion);

如果我们限制了topic级别的消息缓存数(通常没有),那么会将本地每个queue的缓存数更新为pullThresholdForTopic / currentQueueCount     限制总数 / 队列数

同理,如果我们在topic级别限制了消息缓存的大小(通常没有),那么会将本地每个queue的缓存大小更新为pullThresholdSizeForTopic / currentQueueCount  限制总大小 / 队列数

最后调用this.getmQClientFactory().sendHeartbeatToAllBrokerWithLock();向broker发送心跳,告知这些信息。


接下来我们看看给broker发送心跳信息,主要是执行一下两个方法,

this.sendHeartbeatToAllBroker();
this.uploadFilterClassSource();

跟入第一个方法,首先获取心跳数据
HeartbeatData heartbeatData = this.prepareHeartbeatData();
在此方法中,首先会设置ClientId,然后根据各个consumer给heartbeatData的Set<ConsumerData> getConsumerDataSet设置值,其中ConsumerData,每个ConsumerData包括consumerGroup,获取消费方式(pull/push),消费模型(集群/广播),从哪里开始消费,订阅的tag以及编码。

至于Set<ProducerData> getProducerDataSet(),只给ProducerData设置producerGroup。

然后获取private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable 所有broker的addr,调用下面的方法

int version = this.mQClientAPIImpl.sendHearbeat(addr, heartbeatData, 3000);

将请求码设置成RequestCode.HEART_BEAT,然后对heartbeatData进行编码,最后使用netty发送给broker,这里的超时时间设置的3秒。


而this.uploadFilterClassSource();则是向broker更新文件过滤类,这个我们暂时没用,所以暂时略过,后面可能会在FileterSrv细说。目前可以参考FileterSrv作用

发送完成后会将broker返回的信息用于更新 this.brokerVersionTable.get(brokerName).put(addr, version);

最后RebalanceImplthis.truncateMessageQueueNotMyTopic();会将不再订阅的topic对应的本地的处理队列删除。整个流程大致如下


那么,到这里,DefaultMQPushConsumer拉取消息的整个流程就结束了。里面有些细节的地方没有说得很详细,也是因为DefaultMQPushConsumer拉取消息的确实比较复杂。后面会回过头来补充和总结这部分的。

猜你喜欢

转载自blog.csdn.net/sysong88/article/details/80284338