消息消費方式
Consumer分爲兩種,PullConsumer和PushConsumer。從名字就可以看出一種是拉取的方式,一種是主動Push的方式。具體實現如下:
PullConsumer,由用戶主動調用pull方法來獲取消息,沒有則返回
PushConsumer,在啓動後,Consumer客戶端會主動循環發送Pull請求到broker,如果沒有消息,broker會把請求放入等待隊列,新消息到達後返回response。
所以本質上,兩種方式都是通過客戶端Pull來實現的。
大部分的業務場合下業界用的比較多的是push模式,一句話你沒有特殊需求就用push,push模式可以達到準實時的消息推送
那什麼時候可以用pull模式呢?比如在高併發的場景下,消費端的性能可能會達到瓶頸的情況下,消費端可以採用pull模式,消費端根據自身消費情況去拉取,雖然push模式在消息拉取的過程中也會有流控(當前ProcessQueue隊列有1000條消息還沒有消費或者當前ProcessQueue中最大偏移量和最小偏移量超過2000將會觸發流控,流控的策略就是延遲50ms再拉取消息),但是這個值在實際情況下,可能每臺機器的性能都不太一樣,會不好控制。
消費模式
Consumer有兩種消費模式,broadcast和Cluster,由初始化consumer時設置。對於消費同一個topic的多個consumer,可以通過設置同一個consumerGroup來標識屬於同一個消費集羣。
在Broadcast模式下,消息會發送給group內所有consumer。
在Cluster模式下,每條消息只會發送給group內的一個consumer,但是集羣模式的支持消費失敗重發,從而保證消息一定被消費。
消息消費Demo:
public class Consumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_quick_consumer_name");
consumer.setNamesrvAddr(Const.NAMESRV_ADDR);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.subscribe("test_quick_topic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = msgs.get(0);
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
String msgBody = new String(me.getBody(), RemotingHelper.DEFAULT_CHARSET);
System.out.println("topic: "+topic+" ,tags: "+tags+",keys: "+keys+",body: "+ msgBody);
}catch (Exception e){
e.printStackTrace();
//記錄重試次數
int recousumeTimes = me.getReconsumeTimes();
if(recousumeTimes == 3){
// 記錄日誌......
// 做補償處理
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("consumer start ................");
}
}
消息消費者具體實現類:org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl。
先看下DefaultMQPushConsumer的重要參數
//消費組
private String consumerGroup;
//消費端模式,默認爲集羣模式,還有一種廣播模式
private MessageModel messageModel = MessageModel.CLUSTERING;
//根據消費進度從broker拉取不到消息時採取的策略
//1.CONSUME_FROM_LAST_OFFSET 最大偏移量開始
//2.CONSUME_FROM_FIRST_OFFSET 最小偏移量開始
//3.CONSUME_FROM_TIMESTAMP 從消費者啓動時間戳開始
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
//集羣模式下消息隊列負載策略
private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
//消息過濾關係
private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
//消息消費監聽器
private MessageListener messageListener;
//消息消費進度存儲器
private OffsetStore offsetStore;
//消費線程最小線程數
private int consumeThreadMin = 20;
//消費線程最大線程數,因爲消費線程池用的是無界隊列,所以這個參數用不上,原因請參考線程池原理
private int consumeThreadMax = 64;
//動態調整線程數量的閥值
private long adjustThreadPoolNumsThreshold = 100000;
//併發消費時拉取消息前會有流控,會判斷處理隊列中最大偏移量和最小偏移量的跨度,不能大於2000
private int consumeConcurrentlyMaxSpan = 2000;
//push模式下任務拉取的時間間隔
private long pullInterval = 0;
//每次消費者實際消費的數量,不是從broker端拉取的數量
private int consumeMessageBatchMaxSize = 1;
//從broker端拉取的數量
private int pullBatchSize = 32;
//是否每次拉取之後都跟新訂閱關係
private boolean postSubscriptionWhenPull = false;
//消息最大消費重試次數
private int maxReconsumeTimes = -1;
//延遲將該消息提交到消費者的線程池等待時間,默認1s
private long suspendCurrentQueueTimeMillis = 1000;
//消費超時時間,15分鐘
private long consumeTimeout = 15;
消費者的啓動代碼入口DefaultPushConsumerImpl.start()
方法
public synchronized void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
//1、基本的參數檢查,group name不能是DEFAULT_CONSUMER
this.checkConfig();
//2、將DefaultMQPushConsumer的訂閱信息copy到RebalanceService中
//如果是cluster模式,如果訂閱了topic,則自動訂閱%RETRY%topic
this.copySubscription();
//3、修改InstanceName參數值爲PID
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
//4、新建一個MQClientInstance,客戶端管理類,所有的i/o類操作由它管理
//緩存客戶端和topic信息,各種service
//一個進程只有一個實例
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
//5、Queue分配策略,默認AVG
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
//6、PullRequest封裝實現類,封裝了和broker的通信接口
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
//7、消息被客戶端過濾時會回調hook
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
//8、consumer客戶端消費offset持久化接口
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING://廣播消息本地持久化offset
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING://集羣模式持久化到broker
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
//9、如果是本地持久化會從文件中load
this.offsetStore.load();
//10、消費服務,順序和併發消息邏輯不同,接收消息並調用listener消費,處理消費結果
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
//11、只啓動了清理等待處理消息服務
this.consumeMessageService.start();
//12、註冊(緩存)consumer,保證CID單例
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
this.consumeMessageService.shutdown();
throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
//13、啓動MQClientInstance,會啓動PullMessageService和RebalanceService
mQClientFactory.start();
log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
...
...
default:
break;
}
//14、從NameServer更新topic路由和訂閱信息
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();//如果是SQL過濾,檢查broker是否支持SQL過濾
//15、發送心跳,同步consumer配置到broker,同步FilterClass到FilterServer(PushConsumer)
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
//16、做一次re-balance
this.mQClientFactory.rebalanceImmediately();
}
checkConfig(),檢查配置信息,主要檢查消費者組(consumeGroup)、消息消費方式(messageModel)、消息消費開始偏移量(consumeFromWhere)、消息隊列分配算法(AllocateMessageQueueStrategy)、訂閱消息主題(Map<topic,sub expression ),消息回調監聽器(MessageListener)、順序消息模式時是否只有一個消息隊列等等。
copySubscription().加工訂閱信息,將Map<String /* topic*/, String/* subExtentions*/>轉換爲Map<String,SubscriptionData>,同時,如果消息消費模式爲集羣模式,還需要爲該消費組創建一個重試主題。
第4步,初始化一個MQClientInstance,
這個實例在一個JVM中消費者和生產者共用,MQClientManager中維護了一個factoryTable,類型爲ConcurrentMap,保存了clintId和MQClientInstance。
第5步,對於同一個group內的consumer,RebalanceImpl
負責分配具體每個consumer應該消費哪些queue上的消息,以達到負載均衡的目的。Rebalance支持多種分配策略,比如平均分配、一致性Hash等(具體參考AllocateMessageQueueStrategy
實現類)。默認採用平均分配策略(AVG)。
比如現在有4個消息隊列(q1,q2,q3,q4),3個消費者(m1,m2,m3),那麼消費者與消息隊列的對應關係是什麼呢?我們按照一個輪詢算法來表示, m1(q1,q4) m2(q2) m3(q3),如果此時q2消息隊列失效(所在的broker掛了),那麼消息隊列的消費就需要重新分配,RebalanceImpl 就是幹這事的,該類的調用軌跡如下:(MQClientInstance start --> (this.rebalanceService.start()) ---> RebalanceService.run(this.mqClientFactory.doRebalance()) ---> MQConsumerInner.doRebalance(DefaultMQPushConsumerImpl) --->RebalanceImpl.doRebalance。
在這裏着重說明一點:消息隊列數量與消費者關係:1個消費者可以消費多個隊列,但1個消息隊列只會被一個消費者消費;如果消費者數量大於消息隊列數量,則有的消費者會消費不到消息(集羣模式)。
MQClientInstance
下面看一下this.mQClientFactory =MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
String clientId = clientConfig.buildMQClientId();
MQClientInstance instance = this.factoryTable.get(clientId);
if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
if (prev != null) {
instance = prev;
log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}
}
return instance;
}
public String buildMQClientId() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClientIP());
sb.append("@");
sb.append(this.getInstanceName());
if (!UtilAll.isBlank(this.unitName)) {
sb.append("@");
sb.append(this.unitName);
}
return sb.toString();
}
private String clientIP = RemotingUtil.getLocalAddress();
從這段代碼可以看成,一個客戶端 IP@InstanceName 只會持有一個 MQClientInstance 對象,MQClientInstance 無論是消費者還是生產者,都在應用程序這一端。
有了這一層認識,我們就重點關注一下該類的屬性:
ClientConfig clientConfig
配置信息。
int instanceIndex
MQClientInstance在同一臺機器上的創建序號。
String clientId
客戶端id。
ConcurrentMap<String/* group */, MQProducerInner> producerTable
生產組--》消息生產者,也就是在應用程序一端,每個生產者組在同一臺應用服務器只需要初始化一個生產者實例。
ConcurrentMap<String/* group */, MQConsumerInner> consumerTable
消費組--》消費者,也就是在應用程序一 端 ,每個消費組,在同一臺應用服務器只需要初始化一個消費者即可。
ConcurrentMap<String/* group */, MQAdminExtInner> adminExtTable
主要是處理運維命令的。
NettyClientConfig nettyClientConfig
網絡配置。
MQClientAPIImpl mQClientAPIImpl
MQ 客戶端實現類。
MQAdminImpl mQAdminImpl
MQ 管理命令實現類。
ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable
topic 路由信息。
ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable
broker信息,這些信息存在於NameServer,但緩存在本地客戶端,供生產者、消費者共同使用。
ClientRemotingProcessor clientRemotingProcessor
客戶端命令處理器。
PullMessageService pullMessageService
消息拉取線程,一個MQClientInstance 只會啓動一個消息拉取線程。
RebalanceService rebalanceService
隊列動態負載線程。
DefaultMQProducer defaultMQProducer
默認的消息生產者。
ConsumerStatsManager consumerStatsManager
消費端統計。
AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0)
心跳包發送次數。
ServiceState serviceState = ServiceState.CREATE_JUST
狀態。
MQClientInstance mq客戶端實例,每臺應用服務器將持有一個MQClientInstance對象,供該應用服務器的消費者,生產者使用。該類是消費者,生產者網絡處理的核心類。
然後回到DefaultPushConsumerImpl.start()
方法中的this.mQClientFactory.start();啓動MQClientInstance
public void start() throws MQClientException {
synchronized(this) {
switch(this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
this.mQClientAPIImpl.start();
// 1、Start various schedule tasks
this.startScheduledTask();
// 2、Start pull service,開始處理PullRequest
this.pullMessageService.start();
// 3、Start rebalance service
this.rebalanceService.start();
// 4、Start push service,consumer預留的producer,發送要求重新的消息
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
this.log.info("the client factory [{}] start OK", this.clientId);
this.serviceState = ServiceState.RUNNING;
case RUNNING:
case SHUTDOWN_ALREADY:
default:
return;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", (Throwable)null);
}
}
}
1.this.startScheduledTask();啓動了定時任務,看下啓動了哪裏定時任務
private void startScheduledTask() {
//每隔2分鐘嘗試獲取一次NameServer地址
if (null == this.clientConfig.getNamesrvAddr()) {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
} catch (Exception var2) {
MQClientInstance.this.log.error("ScheduledTask fetchNameServerAddr exception", var2);
}
}
}, 10000L, 120000L, TimeUnit.MILLISECONDS);
}
////每隔30S嘗試更新主題路由信息
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
} catch (Exception var2) {
MQClientInstance.this.log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", var2);
}
}
}, 10L, (long)this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
//每隔30S 進行Broker心跳檢測
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
MQClientInstance.this.cleanOfflineBroker();
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
} catch (Exception var2) {
MQClientInstance.this.log.error("ScheduledTask sendHeartbeatToAllBroker exception", var2);
}
}
}, 1000L, (long)this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
//默認每隔5秒持久化ConsumeOffset
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
MQClientInstance.this.persistAllConsumerOffset();
} catch (Exception var2) {
MQClientInstance.this.log.error("ScheduledTask persistAllConsumerOffset exception", var2);
}
}
}, 10000L, (long)this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
//默認每隔1S檢查線程池適配
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
MQClientInstance.this.adjustThreadPool();
} catch (Exception var2) {
MQClientInstance.this.log.error("ScheduledTask adjustThreadPool exception", var2);
}
}
}, 1L, 1L, TimeUnit.MINUTES);
}
2.this.pullMessageService.start();
啓動了pullMessageService服務線程,這個服務線程的作用就是拉取消息,我們去看下他的run方法:
@Override
public void run() {
while (!this.isStopped()) {
try {
//從LinkedBlockingQueue中拉取pullRequest
PullRequest pullRequest = this.pullRequestQueue.take();
this.pullMessage(pullRequest);
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
從pullRequestQueue中獲取pullRequest,如果pullRequestQueue爲空,那麼線程將阻塞直到有pullRequest放入,那麼pullRequest是什麼時候放入的呢,有2個地方:
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");
}
}
public void executePullRequestImmediately(final PullRequest pullRequest) {
try {
this.pullRequestQueue.put(pullRequest);
} catch (InterruptedException e) {
log.error("executePullRequestImmediately pullRequestQueue.put", e);
}
}
executePullRequestImmediately 和 executePullRequestLater,一個是立即放入pullRequest,一個是延遲放入pullRequest,什麼時候需要延遲放入pullRequest呢,都是出現異常的情況下,什麼時候立即放入呢,我們看下這個方法的調用鏈:
有3個地方調用,去掉延遲調用的那一處,還有兩處
- 第一個是RebalancePushImpl#dispatchPullRequest中創建,這個是消息隊列的負載均衡,- 第二個是消息拉取完成之後,又重新把pullRequest放入pullRequestQueue中
我們看下PullRequest類
public class PullRequest {
private String consumerGroup;
private MessageQueue messageQueue;
private ProcessQueue processQueue;
private long nextOffset;
private boolean lockedFirst = false;
...
}
consumerGroup 消費組
messageQueue 消費隊列
ProcessQueue 承載拉取到的消息的對象
nextOffset 下次拉取消息的點位
如果從pullRequestQueue中take到pullRequest,那麼執行this.pullMessage(pullRequest);
private void pullMessage(PullRequest pullRequest) {
MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
if (consumer != null) {
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl)consumer;
impl.pullMessage(pullRequest);
} else {
this.log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
}
}
這裏的consumer直接強制轉換成了DefaultMQPushConsumerImpl,爲什麼呢,看來這個拉取服務只爲push模式服務,那麼pull模式呢,TODO一下,回頭看看,進入DefaultMQPushConsumerImpl.pullMessage方法,這個方法就是整個消息拉取的關鍵方法
public void pullMessage(final PullRequest pullRequest) {
//1.獲取處理隊列ProcessQueue
final ProcessQueue processQueue = pullRequest.getProcessQueue();
//2.如果dropped=true,那麼return
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
//3.然後更新該消息隊列最後一次拉取的時間
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
try {
//4.如果消費者 服務狀態不爲ServiceState.RUNNING,默認延遲3秒再執行
this.makeSureStateOK();
} catch (MQClientException e) {
log.warn("pullMessage exception, consumer state not ok", e);
//4.1 這個方法在上文分析過,延遲執行放入pullRequest操作
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
return;
}
//5.是否暫停,如果有那麼延遲3s執行,目前我沒有發現哪裏有調用暫停,可能是爲以後預留
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;
}
消費進度,消費等等功能的底層核心數據保存都是有ProcessQueue提供,類似於消息快照的意思,主要是因爲在消息拉取到的時候,會把消息存放在其中。
這裏想說一下很多地方都用到了狀態,是否停止,暫停這樣的屬性,一般都是用volatile去修飾,在不同線程中起到通信的作用。
//6.消息的拉取會有流量控制,當processQueue沒有消費的消息的數量達到(默認1000個)會觸發流量控制
long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
//PullRequest延遲50ms後,放入LinkedBlockQueue中,每觸發1000次打印一次警告
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;
}
//7.當processQueue中沒有消費的消息體總大小 大於(默認100m)時,觸發流控,
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;
}
//8.如果不是順序消息,判斷processQueue中消息的最大間距,就是消息的最大位置和最小位置的差值如果大於默認值2000,那麼觸發流控
if (!this.consumeOrderly) {
if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
log.warn(
"the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
pullRequest, queueMaxSpanFlowControlTimes);
}
return;
}
上面的6,7,8步都在進行流控判斷,防止消費端壓力太大,未消費消息太多
//9.獲取主題訂閱信息
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
這裏通過pullRequest的messageQueue獲取topic,再從rebalanceImpl中通過topic獲取SubscriptionData,作用是去broker端拉取消息的時候,broker端要知道拉取哪個topic下的信息,過濾tag是什麼
//10.new一個回調方法,這個回調方法在broker端拉取完消息將調用
PullCallback pullCallback = new PullCallback() {
這一步的回調方法後文再分析,先過
//11.如果是集羣消費模式,從內存中獲取MessageQueue的commitlog偏移量
boolean commitOffsetEnable = false;
long commitOffsetValue = 0L;
if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
if (commitOffsetValue > 0) {
commitOffsetEnable = true;
從內存中獲取MessageQueue的commitLog偏移量,爲什麼是從內存中獲取呢,集羣模式消費進度不是存儲在broker端的嗎?這個問題我們留着,等我們分析消費進度機制的時候再來看
String subExpression = null;
boolean classFilter = false;
//12.這裏又去獲取了一遍SubscriptionData,上面不是獲取了嗎,沒有必要的感覺
SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
if (sd != null) {
if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) {
//過濾信息
subExpression = sd.getSubString();
}
//是否是類過濾模式,現在已經不建議用類過濾模式了,5.0版本之後將棄用
classFilter = sd.isClassFilterMode();
}
//13.構建拉取消息系統Flag: 是否支持comitOffset,suspend,subExpression,classFilter
int sysFlag = PullSysFlag.buildSysFlag(
commitOffsetEnable, // commitOffset
true, // suspend
subExpression != null, // subscription
classFilter // class filter
);
//14.調用pullAPI方法來拉取消息
this.pullAPIWrapper.pullKernelImpl(
pullRequest.getMessageQueue(), // 消息消費隊列
subExpression,//消息訂閱子模式subscribe( topicName, "模式")
subscriptionData.getExpressionType(),
subscriptionData.getSubVersion(),// 版本
pullRequest.getNextOffset(),//拉取位置
this.defaultMQPushConsumer.getPullBatchSize(),//從broker端拉取多少消息
sysFlag,// 系統標記,FLAG_COMMIT_OFFSET FLAG_SUSPEND FLAG_SUBSCRIPTION FLAG_CLASS_FILTER
commitOffsetValue,// 當前消息隊列 commitlog日誌中當前的最新偏移量(內存中)
BROKER_SUSPEND_MAX_TIME_MILLIS, // 允許的broker 暫停的時間,毫秒爲單位,默認爲15s
CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, // 超時時間,默認爲30s
CommunicationMode.ASYNC, // 超時時間,默認爲30s
pullCallback // pull 回調
);
進入PullAPIWrapper#pullKernelImpl,看下具體的拉取實現
//15.查找Broker信息
FindBrokerResult findBrokerResult =
this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
this.recalculatePullFromWhichNode(mq), false);
//16.如果沒有找到對應的broker,那麼重新從nameServer拉取信息
if (null == findBrokerResult) {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
findBrokerResult =
this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
this.recalculatePullFromWhichNode(mq), false);
}
根據brokerName,brokerId從mQClientFactory中查找Broker信息,最後調用MQClientAPIImpl#pullMessage去broker端拉取消息,MQClientAPIImpl封裝了網絡通信的一些API,我們找到broker端處理拉取請求的入口,根據RequestCode.PULL_MESSAGE搜索,找到PullMessageProcessor#processRequest方法:
final GetMessageResult getMessageResult =
this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);
這個方法調用了MessageStore.getMessage()獲取消息,方法的參數的含義:
String group, 消息組名稱
String topic, topic名稱
int queueId, 隊列ID,就是ConsumerQueue的ID
long offset, 待拉取偏移量
int maxMsgNums, 最大拉取數量
MessageFilter messageFilter 消息過濾器
GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
//待查找的隊列的偏移量
long nextBeginOffset = offset;
//當前隊列最小偏移量
long minOffset = 0;
//當前隊列最大偏移量
long maxOffset = 0;
GetMessageResult getResult = new GetMessageResult();
//當前commitLog最大偏移量
final long maxOffsetPy = this.commitLog.getMaxOffset();
//根據topicId和queueId獲取consumeQueue
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
minOffset = consumeQueue.getMinOffsetInQueue();
maxOffset = consumeQueue.getMaxOffsetInQueue();
//當前隊列沒有消息
if (maxOffset == 0) {
status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
nextBeginOffset = nextOffsetCorrection(offset, 0);
} else if (offset < minOffset) {
status = GetMessageStatus.OFFSET_TOO_SMALL;
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else if (offset == maxOffset) {
status = GetMessageStatus.OFFSET_OVERFLOW_ONE;
nextBeginOffset = nextOffsetCorrection(offset, offset);
} else if (offset > maxOffset) {
status = GetMessageStatus.OFFSET_OVERFLOW_BADLY;
if (0 == minOffset) {
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else {
nextBeginOffset = nextOffsetCorrection(offset, maxOffset);
}
}
- maxOffset == 0
- offset < minOffset
- offset == maxOffset
- offset > maxOffset
這四種情況下都是異常情況,只有minOffset <= offset <= maxOffset情況下才是正常,從commitLog獲取到數據之後,返回
getResult.setStatus(status);
getResult.setNextBeginOffset(nextBeginOffset);
getResult.setMaxOffset(maxOffset);
getResult.setMinOffset(minOffset);
這裏minOffset和maxOffset都是broker端的consumeQueue中的最大最小值,現在已經從commitLog中拿到了需要消費的消息,回到PullMessageProcessor#processRequest中
if (getMessageResult.isSuggestPullingFromSlave()) {
responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly());
} else {
responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
}
如果從節點數據包含下一次拉取偏移量,設置下一次拉取任務的brokerId,如果commitLog標記可用並且當前節點爲主節點,那麼更新消息的消費進度,關於消費進度後文會單獨分析
服務端消息拉取處理完畢之後,我們回到consumer端,我們進入MQClientAPIImpl#processPullResponse
PullStatus pullStatus = PullStatus.NO_NEW_MSG;
switch (response.getCode()) {
case ResponseCode.SUCCESS:
pullStatus = PullStatus.FOUND;
break;
case ResponseCode.PULL_NOT_FOUND:
pullStatus = PullStatus.NO_NEW_MSG;
break;
case ResponseCode.PULL_RETRY_IMMEDIATELY:
pullStatus = PullStatus.NO_MATCHED_MSG;
break;
case ResponseCode.PULL_OFFSET_MOVED:
pullStatus = PullStatus.OFFSET_ILLEGAL;
break;
default:
throw new MQBrokerException(response.getCode(), response.getRemark());
}
根據服務端返回的結果code碼來處理拉取結果,組裝PullResultExt
return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(),
responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody());
現在調用pullCallback.onSuccess(pullResult);我們進入pullCallback中:
pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
subscriptionData);
pullAPIWrapper.processPullResult是處理拉取到的消息進行解碼成一條條消息,並且執行tag模式的消息過濾,並且執行hook操作並填充到MsgFoundList中,接下來按照正常流程分析,就是拉取到消息的情況
long prevRequestOffset = pullRequest.getNextOffset();
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
...
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
先跟新下一次拉取的偏移量,如果MsgFoundList爲空,那麼立即觸發下一次拉取,爲什麼可能爲空呢,因爲有tag過濾,服務端只驗證了tag的hashcode,爲什麼要採用這樣的方式呢,還有個疑問就是,這個時候被過濾掉的消息怎麼才能被其他消費者消費,因爲broker端已經提交了消費進度。
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
//消費消息服務提交
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
pullResult.getMsgFoundList(),
processQueue,
pullRequest.getMessageQueue(),
dispatchToConsume);
將msgFoundList提交保存到processQueue中,承載的對象是msgTreeMap,processQueue中用到了讀寫鎖,後面分析一下,然後將拉取到的消息提交到consumeMessageService線程中,這裏將是消費消息的入口處,到這裏消息的拉取就完成了,這次消息拉取完成後,pullRequest將會被重新放入pullrequestQueue中,再次進行消息的拉取。下面一張流程圖就是消息拉取的整個過程