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
最终分配情况大致如下:
sonsumer0 consumer1 consumer2
queue0 queue3 queue6
queue1 queue4 queue7
queue2 queue5
3.7)存储订阅关系的subscription不能为空,private Map
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.defaultMQPushConsumer, this.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
14、将DefaultMQPushConsumerImpl注册到客户端实例中,也就是往ConcurrentMap
若未能成功注册,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
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
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
A)遍历List
B)更新ProcessQueue.msgCount变量,记录消息个数;
C)经过第A步处理之后,若msgTreeMap变量不是空并且ProcessQueue.consuming为false(初始化为false)则置consuming为true(在该msgTreeMap变量消费完之后再置为false)、置临时变量dispatchToConsume为true;否则置临时变量dispatchToConsume为false表示没有待消费的消息或者msgTreeMap变量中存入了数据还未消费完,在没有消费完之前不允许在此提交消费请求,在消费完msgTreeMap之后置consuming为false;
D)取List
E)返回临时变量dispatchToConsume值; //此处的msgTreeMap很重要,流控就依赖他的一些属性
8.9)调用ConsumeMessageService.submitConsumeRequest(List
A)若是 顺序消费 ,则调用ConsumeMessageOrderlyService. submitConsumeRequest(List
B)若是 并发消费 ,则调用ConsumeMessageConcurrentlyService.submitConsumeRequest(List
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
调用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
C)以PullRequest对象的messageQueue变量为参数调用RebalanceImpl.removeProcessQueue(MessageQueue mq)方法,在该方法中,首先从RebalanceImpl.processQueueTable: ConcurrentHashMap
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
this
.topicSubscribeInfoTable.get(topic);
List
this
.mQClientFactory.findConsumerIdList(topic, consumerGroup);
|
将这两个集合排序,然后将他们作为参数调用此方法,做负载均衡
List
this
.consumerGroup,
this
.mQClientFactory.getClientId(),mqAll,cidAll);
|
说白了就是,我告诉你我这个消费者组有几个消费者,我要订阅的topic下的所有的broker的queue的集合,你给我用指定策略分配一下,看看我的当前这个消费者应该订阅那几个queue。
//然后调用
updateProcessQueueTableInRebalance(
final
String topic,
final
Set
final
boolean
isOrder);
//在负载均衡线程内
ConcurrentMap
如果发现传入的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
20
、
|
到此我们的负载均衡后,订阅队列改变后,还要通知broker:public void messageQueueChanged(String topic, Set
在此方法中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
至于Set
然后获取private final ConcurrentMap
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拉取消息的确实比较复杂。后面会回过头来补充和总结这部分的。