更多請移步我的博客
對比看兩種消費方式的實現:順序消費與併發消費。這裏對順序消費只關注消費端,不關心producer與broker怎麼處理順序消息,假設架構及策略已保證消息的全局或者局部順序性。通過構建假定前提,我們可以忽略本次討論的非重點內容。以下仍以Push方式爲例。
消費方式
宏觀上看rmq自身是一個生產-消費模式,在他各個角色的具體實現中也不乏生產-消費模式的使用。DefaultMQPushConsumerImpl
中消息的拉取及拉取成功後的消費均採用生產-消費的方式進行組織(PullRequest
、ConsumeRequest
),消費的流程在之前rocketmq-消息重複分析一文中記錄的很清楚,不再展開。
不論是順序還是併發消費,都使用ProcessQueue
做爲本地消息的存儲介質,每個MessageQueue
對應一個ProcessQueue
,該關係保存在RebalanceImpl
中,每次複雜均衡時可能發生改變。ProcessQueue
內部用TreeMap<Long, MessageExt>
來保存消息,key是消息的offset。TreeMap
內部使用的紅黑樹,根據樹的特性可知消息的本地存儲是offset有序的,併發消費時DefaultMQPushConsumerImpl
去拉取消息會根據offset(RebalanceImpl#getMaxSpan)的跨度判斷是否限流。
併發
關於併發消費的源碼在rocketmq-消息重複分析中已有分析。
這裏在強調下其ack機制的實現,ack是發送響應的過程,來確保消息的送達,在rmq實現中,爲了確保消息“至少消費一次”語義,採用 offset + sendback 的方式來實現。
在併發消費下,不論消費成功還是失敗offset都會記錄爲本次消費的最大一個offset,對於消費失敗的消息,rmq的consumer會再次發回broker,如果此步驟也失敗,降級爲本地延遲消費,然後重複消費步驟。
順序
順序分全局順序和局部順序。全局有序的話就一個隊列一個消費者;局部有序情況下,按照某個業務爲度將統一緯度的消息發送到指定隊列(可以通過自定義發送策略實現),消費者順序消費分配到的隊列消息。PS:廣播與集羣略有差異,以下默認集羣消費。
在rocketmq-消息重複分析中有談到負載均衡的流程,在負載均衡完成的最後有這麼一個操作:
// RebalanceImpl
// omit
// Rebalance後,更新本地的queue信息,消費者提交PullRequest,從新隊列拉取消息
this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
//omit
這方法的三個參數意義是:當前消費者分配到哪個topic下的哪些隊列,當前消費者的消費方式是否爲順序。
下面的代碼需要注意對isOrder
的使用。
// RebalanceImpl
// 1. 刪除已經不再訂閱的messageQueue
// omit
// 2. 訂閱新的messageQueue,並封裝新的PullRequest
List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
for (MessageQueue mq : mqSet) {
// 如果爲新增messageQueue,需要添加到本地記錄
if (!this.processQueueTable.containsKey(mq)) {
// 如果爲順序消費,並且鎖messageQueue失敗,則忽略該messageQueue
if (isOrder && !this.lock(mq)) {
log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
continue;
}
// 刪除之前的消費進度
this.removeDirtyOffset(mq);
ProcessQueue pq = new ProcessQueue();
// 計算消費進度,參見 ConsumeFromWhere,該值在啓動消費者時設置
long nextOffset = this.computePullFromWhere(mq);
// 進行本地存根,若爲新存根則發起PullRequest
if (nextOffset >= 0) {
ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
if (pre != null) {
log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
} else {
log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
PullRequest pullRequest = new PullRequest();
pullRequest.setConsumerGroup(consumerGroup);
pullRequest.setNextOffset(nextOffset);
pullRequest.setMessageQueue(mq);
pullRequest.setProcessQueue(pq);
pullRequestList.add(pullRequest);
changed = true;
}
} else {
log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
}
}
}
// 3. 提交PullRequest進行消息拉取
// omit
我們看到在爲順序消費時,多了一步對messageQueue的加鎖操作,看他做了些什麼事情。
// RebalanceImpl
public boolean lock(final MessageQueue mq) {
// 找到該messageQueue所在的broker,該broker必須是master並且必須是該messageQueue所在的broker
FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
if (findBrokerResult != null) {
LockBatchRequestBody requestBody = new LockBatchRequestBody();
requestBody.setConsumerGroup(this.consumerGroup);
requestBody.setClientId(this.mQClientFactory.getClientId());
requestBody.getMqSet().add(mq);
try {
// 請求broker鎖定該messageQueue
// broker在RebalanceLockManager中嘗試鎖定messageQueue,若鎖定成功則保存MessageQueue與clientId的映射關係
// 一個messageQueue只能被一個client鎖定,來確保一個messageQueue在順序情況下只能被一個消費者訂閱,保證消費的順序性
Set<MessageQueue> lockedMq =
this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
for (MessageQueue mmqq : lockedMq) {
// 更新本地存根
ProcessQueue processQueue = this.processQueueTable.get(mmqq);
if (processQueue != null) {
processQueue.setLocked(true);
// 記錄鎖定時間
processQueue.setLastLockTimestamp(System.currentTimeMillis());
}
}
// 返回鎖定結果
return lockedMq.contains(mq);
} catch (Exception e) {
log.error("lockBatchMQ exception, " + mq, e);
}
}
return false;
}
在順序消費的模式下,負載均衡時需要鎖定隊列來避免多個消費者同時訂閱一個messageQueue的情況,併發消費模式不需要考慮這些問題。在消息的拉取時,順序消費也略有不同。我們以Push方式爲例來看。
// DefaultMQPushConsumerImpl
// 檢查messageQueue是否被鎖定
if (processQueue.isLocked()) {
// 如果是第一次拉取該messageQueue
if (!pullRequest.isLockedFirst()) {
final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
boolean brokerBusy = offset < pullRequest.getNextOffset();
log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
pullRequest, offset, brokerBusy);
if (brokerBusy) {
log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
pullRequest, offset);
}
pullRequest.setLockedFirst(true);
pullRequest.setNextOffset(offset);
}
} else {
// 如果messageQueue未被鎖定,等待一段時間再次嘗試拉取,ConsumeMessageOrderlyService啓動時會啓動任務定時去鎖定所有消費的messageQueue
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
log.info("pull message later because not locked in broker, {}", pullRequest);
return;
}
在MessageListenerOrderly
接口中有這麼一段註釋A MessageListenerOrderly object is used to receive asynchronously delivered messages orderly. one queue,one thread
。意思是順序消費一個queue對應一個消費線程。因爲要順序消費,必然要保證前一個消費後才能消費後面一個,所以多線程在此處沒有存在的必要性。
// ConsumeMessageOrderlyService
// 如果發生負載均衡當前消費者不再處理該messageQueue時,processQueue會被標記爲刪除
if (this.processQueue.isDropped()) {
log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}
// 每個messageQueue對應一把鎖,同一時刻只能有一個線程在消費一個messageQueue
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
// 檢查消費模式及鎖的有效性
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
final long beginTime = System.currentTimeMillis();
for (boolean continueConsume = true; continueConsume; ) {
if (this.processQueue.isDropped()) {
log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
break;
}
// 集羣模式下檢查是否上鎖
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
&& !this.processQueue.isLocked()) {
log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
break;
}
// 集羣模式下檢查鎖是否過期
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
&& this.processQueue.isLockExpired()) {
log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
break;
}
// 消費循環時長不能超過MAX_TIME_CONSUME_CONTINUOUSLY
long interval = System.currentTimeMillis() - beginTime;
if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
break;
}
// 一次消費的消息數量,默認爲1
final int consumeBatchSize =
ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
// 從本地緩存獲取消息
List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
if (msgs.isEmpty()) {
break;
}
final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
ConsumeOrderlyStatus status = null;
ConsumeMessageContext consumeMessageContext = null;
// omit hook
long beginTimestamp = System.currentTimeMillis();
ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
boolean hasException = false;
try {
// 對processQueue加上消費鎖,防止負載均衡時可能會發生爭搶
this.processQueue.getLockConsume().lock();
if (this.processQueue.isDropped()) {
log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
this.messageQueue);
break;
}
// 參考 ConsumeOrderlyStatus
status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
} catch (Throwable e) {
hasException = true;
} finally {
this.processQueue.getLockConsume().unlock();
}
// omit hook and stats preprocess
// 拋出異常時
if (null == status) {
status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
// omit hook and stats
// 處理消費結果
continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
}
} else {
if (this.processQueue.isDropped()) {
log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}
// 掛起片刻再次嘗試消費
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
}
}
再來看下怎麼處理消費結果。
// ConsumeMessageOrderlyService
public boolean processConsumeResult(
final List<MessageExt> msgs,
final ConsumeOrderlyStatus status,
final ConsumeOrderlyContext context,
final ConsumeRequest consumeRequest
) {
boolean continueConsume = true;
long commitOffset = -1L;
if (context.isAutoCommit()) {
switch (status) {
case COMMIT:
case ROLLBACK:
log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
consumeRequest.getMessageQueue());
case SUCCESS:
// 清空consumingMsgOrderlyTreeMap,並更新一些統計信息,返回下一個offset的起始
commitOffset = consumeRequest.getProcessQueue().commit();
break;
case SUSPEND_CURRENT_QUEUE_A_MOMENT:
// 檢查消息是否已經超過最大消費次數
// 如果沒有超過,本地繼續嘗試消費
// 如果超過,將消息發送到死信隊列,不在處理
// 如果發送到死信隊列失敗,本地繼續嘗試消費
if (checkReconsumeTimes(msgs)) {
consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
this.submitConsumeRequestLater(
consumeRequest.getProcessQueue(),
consumeRequest.getMessageQueue(),
context.getSuspendCurrentQueueTimeMillis());
continueConsume = false;
} else {
commitOffset = consumeRequest.getProcessQueue().commit();
}
break;
default:
break;
}
} else {
// omit
}
// 更新消費偏移
if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
}
return continueConsume;
}
offset保存
offset的持久化可以在本地也可以在遠程(broker上,默認RemoteBrokerOffsetStore
),offset並不是每次消費完成都會向broker發起持久化請求,有這麼幾個持久化入口:
- 定時任務,默認5S一次
- 每次去broker拉取消息時
- 消費者shutdown時
後記
ProcessQueue
中已offset爲key做本地排序怎麼能保證消息的順序呢?很簡單,因爲broker是順序寫commitLog,所以後來消息的offset一定比先到的消息offset大。
在ProcessQueue
中爲順序消費保留了一個consumingMsgOrderlyTreeMap
字段,該字段保存某次消費的消息,爲什麼需要做這個字段呢?猜測只是爲了應對批量消費的,即ConsumeMessageBatchMaxSize大於1時。
小生不才,以上如有描述有誤的地方還望各位不吝賜教 !_!
貼出的源碼均基於release-4.3.2,爲了更好的表達描述的重點,貼出的源代碼會有所刪減。