驚呆:RocketMQ順序消息,是“4把鎖”實現的(順序消費)

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


驚呆:RocketMQ順序消息,是“4把鎖”實現的(順序消費)

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試資格,遇到很多關於RocketMQ 的、很重要的面試題:

如何保證RocketMQ消息有序?

RocketMQ 順序消息,底層原理是什麼?

這些題目是非常常見的面試題,回答的時候 有兩個層面

  • 第一個層面:應用 開發層
  • 第二個層面:底層 源碼層

第一個層面開發層面的回答,參考答案請參見尼恩《技術自由圈》前面的一篇文章

阿里面試:如何保證RocketMQ消息有序?如何解決RocketMQ消息積壓?

一般來說,能夠回答到上面的層次,已經非常牛掰了。

但是,如果能夠更上一層樓,去到第二個層面:底層 源碼層,能從Rocketmq源碼層去解答,那就更加讓面試官 “不能自已、口水直流、震驚不已”,當然,實現”offer直提”,“offer自由”。

這裏,尼恩這道面試題以及第二個層面的參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V156版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

特別提示,尼恩的3高架構宇宙,尼恩Java面試寶典,都是持續升級。

《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到公號【技術自由圈】獲取

本文目錄

回顧: 什麼是順序消息

一條訂單產生的三條消息:訂單創建、訂單付款、訂單完成。

上面三消息是有序的,消費時要按照這個順序依次消費纔有意義,但是不同的訂單之間這些消息是可以並行消費的。

什麼是順序消息?順序消息是指對於一個指定的 Topic ,消息嚴格按照先進先出(FIFO)的原則進行消息發佈和消費,即先發布的消息先消費,後發佈的消息後消費。

順序消息分爲兩種:

  • 分區有序消息
  • 全局有序消息

1、分區有序消息

對於指定的一個 Topic ,所有消息根據 Sharding Key 進行區塊分區,同一個分區內的消息按照嚴格的先進先出(FIFO)原則進行發佈和消費。

同一分區內的消息保證順序,不同分區之間的消息順序不做要求。

  • 適用場景:適用於性能要求高,以 Sharding Key 作爲分區字段,在同一個區塊中嚴格地按照先進先出(FIFO)原則進行消息發佈和消費的場景。
  • 示例:電商的訂單創建,以訂單 ID 作爲 Sharding Key ,那麼同一個訂單相關的創建訂單消息、訂單支付消息、訂單退款消息、訂單物流消息都會按照發布的先後順序來消費。

2、全局有序消息

對於指定的一個 Topic ,所有消息按照嚴格的先入先出 FIFO 的順序來發布和消費。

全局順序消息實際上是一種特殊的分區順序消息,即 Topic 中只有一個分區

因此:全局順序和分區順序的實現原理相同,區別在於分區數量上。

因爲分區順序消息有多個分區,所以分區順序消息比全局順序消息的併發度和性能更高

  • 適用場景:適用於性能要求不高,所有的消息嚴格按照 FIFO 原則來發布和消費的場景。
  • 示例:在證券處理中,以人民幣兌換美元爲 Topic,在價格相同的情況下,先出價者優先處理,則可以按照 FIFO 的方式發佈和消費全局順序消息。

應用開發層的實現

如何實現消息有序?

實現順序消息所必要的條件:順序發送、順序存儲、順序消費。

順序存儲環節,RocketMQ 裏的分區隊列 MessageQueue 本身是能保證 FIFO 的。

所以,在應用開發過程中,不能順序消費消息主要有兩個原因:

  • 順序發送環節,消息發生沒有序:Producer 發送消息到 MessageQueue 時是輪詢發送的,消息被髮送到不同的分區隊列,就不能保證 FIFO 了。
  • 順序消費環節,消息消費無序:Consumer 默認是多線程併發消費同一個 MessageQueue 的,即使消息是順序到達的,也不能保證消息順序消費。

我們知道了實現順序消息所必要的條件:順序發送、順序存儲、順序消費。順序存儲 由 Rocketmq 完成,所以,在應用開發層, 消息的順序需要由兩個階段保證:

  • 消息發生有序

  • 消息消費有序

第一個階段:消息發送有序

很簡單,順序消息發送時, RocketMQ 支持將 Sharding Key 相同(例如同一訂單號)的消息序路由到一個隊列中。

在應用開發層面,要實現順序消息發送時,主要涉及到一個組件: 有序分區選擇器 MessageQueueSelector 接口

select 三個參數:

  • mqs 是可以發送的隊列,
  • msg是消息,
  • arg是上述send接口中傳入的Object對象,

select 返回的是該消息需要發送到的隊列。

生產環境中建議選擇最細粒度的分區鍵進行拆分,例如,將訂單ID、用戶ID作爲分區鍵關鍵字,可實現同一終端用戶的消息按照順序處理,不同用戶的消息無需保證順序。

上述例子裏,是以userid 作爲分區分類標準,對所有隊列個數取餘,來對將相同userid 的消息發送到同一個隊列中。

注意,先hash再取模,防止 不同的分區 發生數據傾斜。防止:沒有hash會不均勻度,導致消費者有的 餓的餓死,汗的汗死。

第二個階段:消息消費有序

消息的順序需要由兩個階段保證:

  • 消息發送有序
  • 消息消費有序

RocketMQ 消費過程包括兩種,分別是併發消費和有序消費

  • 併發消費

    併發消費的接口 MessageListenerConcurrently

    併發消費是 RocketMQ 默認的處理方法,

    併發消費 場景,消費者使用線程池技術,可以併發消費多條消息,提升機器的資源利用率。

    默認配置是 20 個線程,所以一臺機器默認情況下,同一瞬間可以消費 20 個消息。

  • 有序消費 MessageListenerOrderly

    有序消費模式 的接口是,MessageListenerOrderly。

    在消費的時候,還需要保證消費者註冊MessageListenerOrderly類型的回調接口,去實現順序消費,如果消費者採用Concurrently並行消費,則仍然不能保證消息消費順序。

    MessageListenerOrderly 有序消息監聽器

下面是一個例子:

順序消費的事件監聽器爲 MessageListenerOrderly,表示順序消費。

  • 併發消費消息時,當消費失敗時,會默認延遲重試16次。
  • 有序消費消息時,重試次數爲 Integer.MAX_VALUE,而且不延遲。

換言之,有序消費場景,如果某一條消息消費失敗且重試始終失敗,將會導致後續的消息無法消費,產生消息的積壓。

所以,順序消費消息時,一定要謹慎處理異常情況。防止消息隊列積壓。

源碼層:4把鎖,保證消息的有序性

特別說明:

在生產端,所有消息根據 ShardingKey 進行分區,相同 ShardingKey 的消息必須被髮送到同一個分區。

所以,生產端的有序性,在源碼層不需要太多處理。

在源碼層只需要關心 消費的有序處理就行。要實現消息的順序消費,至少要達到兩個條件:

  • 第一個條件:一個分區,只能投遞給同一個客戶端
  • 第二個條件:一個客戶端,只能同時一個線程去執行消息的消費。

第一個條件:一個分區,只能投遞給同一個客戶端。怎麼實現呢?使用分佈式鎖去實現。

第二個條件:同一個客戶端,只能同時一個線程去執行消息的消費。怎麼實現呢?使用本地消費鎖去實現。

另外,光兩個鎖還不夠,RocketMQ 爲了實現 broker 服務端分佈式鎖的操作安全,以及本地的操作安全,還使用了額外的兩把鎖去做加強,

所以,爲了保證有序消息的有序投遞,一共用了4把鎖。

4把鎖,保證消息的有序性,具體如下圖所示:

第一把鎖:broker端的分佈式鎖

正常的邏輯,如果保證一個分區,分配到也僅僅分配到一個client,就需要布式鎖,比如redis分佈式鎖。

RocketMQ沒有用redis分佈式鎖,而是自研分佈式鎖,在broker中設置分佈式鎖,所以broker直接充當redis這些角色而已。

所以,在 RocketMQ 的 broker端:

  • 通過分佈式鎖,實現一個分區 queue 綁定到一個消費者client,
  • 並且 broker 設置一個專門的管理器,來管理分佈式鎖。

broker端的分佈式鎖通過 RebalanceLockManager 管理,存儲結構爲

ConcurrentMap<String/* group */, ConcurrentHashMap<MessageQueue, LockEntry>>,

該分佈式鎖保證:

同一個consumerGroup下同一個messageQueue只會被分配給一個consumerClient。

客戶端, 在開始拉消息之前,首先要獲取 queue的 分佈式鎖。

如何獲取 queue的 分佈式鎖呢? 客戶端會通過rpc 命令去發送獲取 queue的 分佈式鎖的請求,

這個命令,在Broker端,鎖定隊列的請求由AdminBrokerProcessor處理器的lockBatchMQ 方法去 處理

/**
 * 批量鎖隊列請求
 */
private RemotingCommand lockBatchMQ(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    LockBatchRequestBody requestBody = LockBatchRequestBody.decode(request.getBody(), LockBatchRequestBody.class);

    // 通過再平衡鎖管理器去鎖消息隊列,返回鎖定成功的消費隊列
    // 鎖定失敗就代表消息隊列被別的消費者鎖住了並且還沒有過期
    Set<MessageQueue> lockOKMQSet = this.brokerController.getRebalanceLockManager().tryLockBatch(
        requestBody.getConsumerGroup(),
        requestBody.getMqSet(),
        requestBody.getClientId());

    LockBatchResponseBody responseBody = new LockBatchResponseBody();
    // 將鎖定成功的隊列響應回去
    responseBody.setLockOKMQSet(lockOKMQSet);

    response.setBody(responseBody.encode());
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
    return response;
}

然後調用RebalanceLockManager 管理器的的tryLockBatch 方法,獲取對應的分佈式鎖。

public Set<MessageQueue> tryLockBatch(final String group, final Set<MessageQueue> mqs,
                                      final String clientId) {

    // 存放:目前已被clientId對應的消費者  鎖住的分區
    Set<MessageQueue> lockedMqs = new HashSet<MessageQueue>(mqs.size());
    // 存放:目前已被clientId 嘗試加鎖 而 未鎖住的分區
    Set<MessageQueue> notLockedMqs = new HashSet<MessageQueue>(mqs.size());

    for (MessageQueue mq : mqs) {
        // 判斷分區是否已被clientId對應的消費者鎖住
        if (this.isLocked(group, mq, clientId)) {
            lockedMqs.add(mq);
        } else {
            notLockedMqs.add(mq);
        }
    }

    //clientId 嘗試加鎖 而 未鎖住的分區  ,  存在
    if (!notLockedMqs.isEmpty()) {
        try {

            //進入重入鎖,保證 分區 分配的 原子性

            this.lock.lockInterruptibly();
            try {
                // 該消費組下 分區的 分佈式鎖
                ConcurrentHashMap<MessageQueue, LockEntry> groupValue = this.mqLockTable.get(group);
                // 如果爲空,就創建一個 新的分佈式鎖
                if (null == groupValue) {
                    groupValue = new ConcurrentHashMap<>(32);
                    this.mqLockTable.put(group, groupValue);
                }

                // 對於clientId 鎖定的分區,開始嘗試去鎖定
                for (MessageQueue mq : notLockedMqs) {
                    LockEntry lockEntry = groupValue.get(mq);

                    // 爲空就是該分區 還沒被鎖定,可以直接  鎖定
                    if (null == lockEntry) {
                        lockEntry = new LockEntry();
                        lockEntry.setClientId(clientId);
                        groupValue.put(mq, lockEntry);
                        log.info(
                            "tryLockBatch, message queue not locked, I got it. Group: {} NewClientId: {} {}",
                            group,
                            clientId,
                            mq);
                    }

                    // 如果不爲空,之前被我鎖住,就更新鎖住時間,添加到鎖定隊列中
                    if (lockEntry.isLocked(clientId)) {
                        lockEntry.setLastUpdateTimestamp(System.currentTimeMillis());
                        lockedMqs.add(mq);
                        continue;
                    }
                    // 到這說明 被別的消費者鎖住了

                    String oldClientId = lockEntry.getClientId();
                    // 如果過期了就直接換我鎖住
                    if (lockEntry.isExpired()) {
                        lockEntry.setClientId(clientId);
                        lockEntry.setLastUpdateTimestamp(System.currentTimeMillis());
                        log.warn(
                            "tryLockBatch, message queue lock expired, I got it. Group: {} OldClientId: {} NewClientId: {} {}",
                            group,
                            oldClientId,
                            clientId,
                            mq);
                        lockedMqs.add(mq);
                        continue;
                    }
                    //被其他 消費者鎖定了,告警
                    //然後去 搶佔下一個 分區的分佈式鎖

                    log.warn(
                        "tryLockBatch, message queue locked by other client. Group: {} OtherClientId: {} NewClientId: {} {}",
                        group,
                        oldClientId,
                        clientId,
                        mq);
                }
            } finally {

                // 釋放重入鎖,其他線程,也可以進行 分區的分配
                this.lock.unlock();
            }
        } catch (InterruptedException e) {
            log.error("putMessage exception", e);
        }
    }

    return lockedMqs;
}

第二把鎖:broker端的全局鎖

一個分區配備一把鎖,分佈式鎖this.mqLockTable 是一個 ConcurrentMap。

爲了保證分佈式鎖操作的原子性,brocker設置一個專門的管理器,來管理分佈式鎖。

所以在broker上是兩級鎖。
分佈式鎖this.mqLockTable 是一個 ConcurrentMap

    /**
     * 保存每個消費組消費隊列鎖定情況,
     * 以消費組名爲key,每個消費組可以同時鎖住同一個消費 分區,以消費組爲單位保存
     * 注意,這裏不以topic爲key,因爲每個topic都可能會被多個消費組訂閱,各個消費組互不影響,
     */
private final ConcurrentMap<String/* group */, ConcurrentHashMap<MessageQueue, LockEntry>> mqLockTable =
    new ConcurrentHashMap<String, ConcurrentHashMap<MessageQueue, LockEntry>>(1024);

爲啥需要 額外的全局鎖呢?

broker處理RPC命令的線程可不只有一個, 所以這裏用一個全局鎖,來實現 分佈式鎖操作的原子性

//進入重入鎖,保證 分區 分配的 原子性
//clientId 嘗試加鎖 而 未鎖住的分區  ,  存在
if (!notLockedMqs.isEmpty()) {
    try {

        //進入重入鎖,保證 分區 分配的 原子性
        this.lock.lockInterruptibly();

        操作 分佈式鎖 this.mqLockTable 
            ....
    } finally {

        // 釋放重入鎖,其他線程,也可以進行 分區的分配
        this.lock.unlock();
    }
}

本地消費的兩級鎖

消費者消費消息時,需要保證消息消費順序和存儲順序一致,最終實現消費順序和發佈順序的一致。

雖然MessageListenerOrderly被稱爲有序消費模式,但是仍然是使用的線程池去消費消息。實際上,每一個消費者的的消費端都是採用線程池實現多線程消費的模式,即消費端是多線程消費。

MessageListenerConcurrently是拉取到新消息之後就提交到線程池去消費,而MessageListenerOrderly則是通過加分佈式鎖和本地鎖保證同時只有一條線程去消費一個隊列上的數據。

一個消費者至少需要涉及隊列自動負載、消息拉取、消息消費、位點提交、消費重試等幾個部分。其中,與遠程分佈式鎖有關係的是

  • 自動負載
  • 消息拉取

兩級本地鎖主要涉及到的是

  • 消息消費
  • 位點提交

消息消費這塊,由於涉及線程池去消費消息,所以需要設置一個專門的消費鎖。

對於同一個queue,除了消費之外,還涉及位點提交等,所以,一個分區額外設計一把 分區鎖。加起來,在消費者本地,也是兩級鎖:

消費者自動負載均衡(再平衡)

一個消費者至少需要涉及隊列自動負載、消息拉取、消息消費、位點提交、消費重試等幾個部分。

與遠程分佈式鎖有關係的是

  • 自動負載
  • 消息拉取

兩級本地鎖主要涉及到的是

  • 消息消費
  • 位點提交

MQClientInstance 客戶端實例,會開啓多個異步並行服務:

  • 負載均衡服務 rebalanceService:再平衡服務, 專門進行 queue分區的 再平衡,再分配
  • 消息拉取服務 pullMessageService:專門拉取消息,通過內部實現類DefaultMQPushConsumerImpl 拉取
  • 消息消費線程:ConsumeMessageOrderlyService 有序消息消費

RebalanceService 線程啓動後,會以 20s 的頻率計算每一個消費組的隊列負載。

如果有新分配的隊列。這時候 ConsumeMessageOrderlyService 可以嘗試向Broker 申請分佈式鎖

客戶端獲取分佈式鎖:

前面三個並行服務,首先發生作用的是rebalanceService 負載均衡服務,負責獲取 責任分區。

如果不是 有序消息而是普通消息的話,rebalanceService 負載均衡服務獲取到 分區後,就可以開始拉取消息了。

但是有序消息卻不行, 還需要先去 獲取分佈式鎖。

這個獲取分佈式鎖的操作, 由另外一個 異步 ConsumeMessageOrderlyService 服務去定期獲取,週期是20s。

RebalanceImpl#lockAll()發送同步請求 ,加上分佈式鎖

// 鎖定  分配到 MessageQueue 分區
public void lockAll() {
    // 查詢分配的到的分區
    // key爲broker名稱,value爲該消費者在該broker上分配到的消息分區 , 注意,一個topic 可以在多個broker上建立分區
    HashMap<String /*BrokerName*/, Set<MessageQueue>> brokerMqs = this.buildProcessQueueTableByBrokerName();

    //按照 broker 爲單位進行鎖定
    Iterator<Entry<String, Set<MessageQueue>>> it = brokerMqs.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, Set<MessageQueue>> entry = it.next();
        final String brokerName = entry.getKey();
        final Set<MessageQueue> mqs = entry.getValue();

        if (mqs.isEmpty())
            continue;

        // 向該broker發送 批量鎖消息分區的請求
        FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true);
        if (findBrokerResult != null) {
            LockBatchRequestBody requestBody = new LockBatchRequestBody();
            requestBody.setConsumerGroup(this.consumerGroup);
            requestBody.setClientId(this.mQClientFactory.getClientId());
            requestBody.setMqSet(mqs);

            try {
                // 發送同步請求 ,加上分佈式鎖
                Set<MessageQueue> lockOKMQSet =
                    this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);

                //迭代鎖定的 分區
                for (MessageQueue mq : lockOKMQSet) {
                    // 獲取 ProcessQueue (分區消費快照  Queue consumption snapshot)
                    ProcessQueue processQueue = this.processQueueTable.get(mq);
                    if (processQueue != null) {
                        //如果沒有 鎖定消費快照 ,則消費快照加鎖
                        if (!processQueue.isLocked()) {
                            log.info("the message queue locked OK, Group: {} {}", this.consumerGroup, mq);
                        }

                        processQueue.setLocked(true);
                        processQueue.setLastLockTimestamp(System.currentTimeMillis());
                    }
                }
                for (MessageQueue mq : mqs) {
                    if (!lockOKMQSet.contains(mq)) {
                        ProcessQueue processQueue = this.processQueueTable.get(mq);
                        if (processQueue != null) {
                            processQueue.setLocked(false);
                            log.warn("the message queue locked Failed, Group: {} {}", this.consumerGroup, mq);
                        }
                    }
                }
            } catch (Exception e) {
                log.error("lockBatchMQ exception, " + mqs, e);
            }
        }
    }
}

獲取分佈式鎖之後,在本地, 設置到 消費快照的 locked 標誌

消息拉取服務 pullMessageService

如果不是 有序消息,rebalanceService 負載均衡服務獲取到 分區後,就可以開始拉取消息了。

會創建消息去拉取請求,交個消息拉取服務去異步執行。

pullMessage 方法中,首先判斷有沒有分佈式鎖, 沒有就延遲則延遲3s後再將pullRequest重新放回拉取任務中

判斷有沒有分佈式鎖,是通過 本地快照的標誌位來的。

//對應關係: topic每一個的queue在消費的時候,都會指定一個pullRequest
//可以反向導航: 通過請求,去取得那個 topic的queue
public void pullMessage(final PullRequest pullRequest) {

    ...這個方法太長了

        // 併發消費模式
        // 針對於普通消息
        if (!this.consumeOrderly) {
            ....

        } else {
            // 順序消費模式
            // 針對於順序消息
            // 若是是順序消息,那麼 processQueue  就是須要 上 本地快照 鎖
            // 必須進行同步操作, 保障在消費端不會出現亂序
            if (processQueue.isLocked()) {

                // 如果該 消費分區 是第一次拉取消息 lockedFirst = false ,則先計算拉取偏移量
                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);
                    }

                    // 設置下次拉取的offSet
                    pullRequest.setLockedFirst(true);
                    pullRequest.setNextOffset(offset);
                }
            } else {

                // 如果本地快照 鎖 沒被鎖定,則延遲3s後再將pullRequest重新放回拉取任務中
                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                log.info("pull message later because not locked in broker, {}", pullRequest);
                return;
            }
        }
    ...
}

有分佈式鎖,才拉取消息。

拉取消息後,提交消費。

ConsumeMessageOrderlyService 有序消息消費

前面講到,MQClientInstance 客戶端實例,會開啓多個異步並行服務:

  • 負載均衡服務 rebalanceService :再平衡服務, 專門進行 queue分區的 再平衡,再分配
  • 消息拉取服務 pullMessageService :專門拉取消息,通過內部實現類DefaultMQPushConsumerImpl 拉取
  • 消息消費線程 :ConsumeMessageOrderlyService 有序消息消費

客戶端與遠程分佈式鎖有關係的是

  • 自動負載
  • 消息拉取

兩級本地鎖主要涉及到的是

  • 消息消費
  • 位點提交

ConsumeMessageOrderlyService 有序消息消費 ,在他run方法中

首先獲取分區操作鎖, 這個是一個對象鎖

然後獲取 消費鎖, 這是一個 ReentrantLock 鎖。

@Override
public void run() {
    if (this.processQueue.isDropped()) {
        log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
        return;
    }
    // 獲取消息 分區的對象鎖
    final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
    synchronized (objLock) {
        .....
            // 批量消費消息個數
            final int consumeBatchSize =
            ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
        // 獲取消息內容
        List<MessageExt> msgs = 
            this.processQueue.takeMessags(consumeBatchSize);
        .....
    }

    long beginTimestamp = System.currentTimeMillis();
    ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
    boolean hasException = false;
    try {
        //獲取消費鎖
        this.processQueue.getLockConsume().lock();
        ....

            //  消費消息
            status = messageListener.consumeMessage(
            Collections.unmodifiableList(msgs), context);
    } catch (Throwable e) {
        log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
                 RemotingHelper.exceptionSimpleDesc(e),
                 ConsumeMessageOrderlyService.this.consumerGroup,
                 msgs,
                 messageQueue);
        hasException = true;
    } finally {

        // 釋放消息消費鎖
        this.processQueue.getLockConsume().unlock();
    }
    .....
}

上面的代碼,用到了兩級鎖:

  • 第三級的本地鎖 LockObject:queue分區上級別的 操作鎖。

這個鎖的粒度更大, 不僅僅鎖住 消息的消費操作,還鎖住了位點的提交,以及持續消費的一批消息的操作。

  • 第四級的本地鎖: 分區上的快照 消費鎖

這個鎖的粒度更小, 僅僅鎖住 消息的消費操作,保證同一個消息queue 分區上的消息消費,只有一個線程能夠執行,保證分區消費的次序不會打亂。

4級鎖的總結

我們做一個關於順序消費的總結:

通過4把鎖的機制,消息隊列 messageQueue 的數據都會被消費者實例單線程的執行消費;

當然,假如消費者擴容,消費者重啓,或者 Broker 宕機 ,順序消費也會有一定機率較短時間內亂序,所以消費者的業務邏輯還是要保障冪等

這裏還需要考慮broker 鎖的異常情況,假如一個broke 隊列上的消息被consumer 鎖住了,萬一consumer 崩潰了,這個鎖就釋放不了,所以broker 上的鎖需要加上鎖的過期時間。

注意消息的積壓

在使用順序消息時,一定要注意其異常情況的出現,對於順序消息,當消費者消費消息失敗後,消息隊列 RocketMQ 版會自動不斷地進行消息重試(每次間隔時間爲 1 秒),重試最大值是Integer.MAX_VALUE.這時,應用會出現消息消費被阻塞的情況。

因此,建議您使用順序消息時,務必保證應用能夠及時監控並處理消費失敗的情況,避免消息積壓現象的發生。

關於消息的積壓,參考答案請參見尼恩《技術自由圈》前面的一篇文章

阿里面試:如何保證RocketMQ消息有序?如何解決RocketMQ消息積壓?

關於積壓監控,請參考尼恩的 《RocketMQ 四部曲視頻》,如果能夠回答到上面的層次,已經非常牛掰了。

尼恩的 《RocketMQ 四部曲視頻》,從架構師視角揭祕 RocketMQ 的架構哲學,讓大家徹底的瞭解這個高深莫測 RocketMQ 組件的宏觀架構,提升大家的架構水平和設計水平。

說在最後:有問題可以找老架構取經

RocketMQ 相關的面試題,是非常常見的面試題。

以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走。

在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來改簡歷、做幫扶。

尼恩指導了大量的小夥伴上岸,前段時間,剛指導一個40歲+被裁小夥伴,拿到了一個年薪100W的offer。

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章