阿里面試:說說Rocketmq推模式、拉模式?

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

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

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


阿里面試:說說Rocketmq推模式、拉模式?

尼恩說在前面

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

說說Rocketmq的推模式、拉模式?

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

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

關於Rocketmq 的核心面試題,尼恩前面也梳理過幾篇文章:

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

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

這裏,又新增一個核心面試的答案,“說說Rocketmq的推模式、拉模式?”。

這些文章,在底層都是相同的。幫助大家從Rocketmq源碼層去解答,那就更加讓面試官 “不能自已、口水直流、震驚不已”,然後實現”offer直提”。

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

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

本文目錄

經典的推模式/拉模式

首先,明確一下業務場景

這裏談論的推拉模式,指的是 Consumer 和 Broker 之間,不是 producer與broker之間。

經典的推模式

經典的推模式,指的是消息從 Broker 推向 Consumer。

Consumer 被動的接收消息,由 Broker 來主導消息的發送。作爲代理人,Broker 接受完消息之後,可以立馬推送給 Consumer。

Consumer等着就行,消息會有broker主動推過來。所以 Consumer 的處理策略很簡單。

推模式的缺點: Consumer可能就“消化不良/OOM”。

當 Broker 推送消息的速率大於Consumer消費速率時,Consumer可能就“消化不良”,出現內存積壓,內存溢出,OOM,因爲根本消費不過來啊。

所以,經典的推模式,適用於消息量不大、Consumer消費能力強的場景。

經典的拉模式

經典的拉模式,指的是 Consumer 主動向 Broker 請求拉取消息。

上面講到,經典的推模式,適用於消息量不大、Consumer消費能力強的場景。如果Consumer消費能力弱, 那麼就改變方向好了, 由推改完拉。

拉的話,主動權就在Consumer身上了, 能消化多少,喫多少。

假設當前Consumer 消化不過來、消費不過來了,它可以根據一定的策略,暫停拉取甚至停止拉取,或者間隔拉取都行。

凡事有利必有弊。

拉模式的缺點:消息延遲+消息積壓。 如果Consumer 隔個 2天採取拉取一批,消息就很有可能延遲,甚至出現嚴重的消息延遲。而且Broker 服務端大概率會消息積壓。

推拉模式,如何選型?

選擇推模式的消息隊列中間件,主要有ActiveMQ

選擇推模式的消息隊列中間件,主要有RocketMQ 和 Kafka

雖然RocketMQ 和 Kafka都選擇了拉模式。也就是允許消息延遲 + 允許消息積壓。

所以,選擇RocketMQ 和 Kafka,就需要做好消息積壓的監控。

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

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

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

Rocketmq 的推模式和拉模式

Rocketmq 的客戶端,也定義了兩個模式: 推模式和拉模式

但是實際上,RocketMQ 中的 PushConsumer 推模式,僅僅是披着拉模式的方法,本質還是拉模式。

RocketMQ 中的 PushConsumer 推模式

接下來,我們首先看看RocketMQ 中的 PushConsumer 推模式。

這裏,建議大家看看,尼恩的前面一篇文章:

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

介紹了 RocketMQ 拉取消息的核心流程,具體如下圖所示。

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

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

  • 負載均衡服務 rebalanceService :再平衡服務.

    專門進行 queue分區的 再平衡,再分配,然後發佈拉取消息的請求 pullRequest 實例。

  • 消息拉取服務 pullMessageService:專門負責拉取消息。

    從請求隊列 pullRequestQueue 隊列 獲取一個一個的 pullRequest,

    通過內部實現類DefaultMQPushConsumerImpl 拉取 消息。

    注意,拉取的消息,放在另一個隊列 messageQueue 緩存,拉取之前,會進行流控檢查,如果這個隊列滿了(>1000個消息或者 >100M內存) 則延遲50ms再拉取, 當然,下一次執行拉取之前,同樣也會進行流控檢查

  • 消息消費線程:ConsumeMessageOrderlyService 有序消息消費, 或者 並行消息。 從messageQueue 拉取消息,進行消費。

上面設計3類線程,在3類線程之間,通過兩個隊列進行 同步:

  • 拉取消息的請求隊列 pullRequestQueue
  • 緩存消息的隊列 messageQueue

Rocketmq 的推模式,本質是一種拉模式, 只是爲了讓客戶端不會 累死, 在拉取之前進行流控。

具體請參見 尼恩 《Rocketmq 四部曲視頻》配套的 註釋版源碼:

//   接下來就是消費者的拉取流量控制,閾值爲 1000個消息, 或者 100M
// 消費者消費的太慢了,broker推送的太快了,進行 Flow control

if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {

    // 將pullRequest放入隊列,只不過是經過後臺的定時線程池延50 ms 遲放入,進行 Flow control
    // 流量控制, 減緩拉取消息的速度
    //     * Flow control threshold on queue level, each message queue will cache at most 1000 messages by default,
    //     * Consider the {@code pullBatchSize}, the instantaneous value may exceed the limit
    //     */
    //    private int pullThresholdForQueue = 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;
}

客戶端沒有消息怎麼辦呢?

上一節講到, RocketMQ三類線程,相互配合: 在背後偷偷的幫我們去 Broker拉消息。

第一類線程 RebalanceService ,根據 topic 的隊列數量和消費者個數做負載均衡,對於分配到queue 產生的 pullRequest拉取請求,並講請求 隊列 pullRequestQueue 中。

第二類線程 PullMessageService ,不斷的pullRequestQueue隊列 中獲取 pullRequest,然後從 broker 拉取消息。

PullMessageService 把拉取請求,重新放進pullRequestQueue 隊列, 大致的代碼如下:

//broker 沒有 新消息
case NO_NEW_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());

DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

// 把拉取請求,重新放進隊列

DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
//broker 沒有 匹配消息
case NO_MATCHED_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());

DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
// 把拉取請求,重新放進隊列

DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);

那麼,如果broker暫時沒有消息,怎麼辦呢?

Broker 處理拉取消息命令的 處理器,叫做 PullMessageProcessor。

PullMessageProcessor 裏面的 processRequest 方法是用來處理pullRequest 拉消息請求.

如果broker有消息, processRequest 方法就直接返回,

如果broker沒有消息, processRequest 方法怎麼辦呢?

我們來看一下代碼。

我們再來看下 suspendPullRequest 方法做了什麼。

這裏有個broker 異步線程 PullRequestHoldService

這個線程會每 5 秒從 pullRequestTable 取PullRequest請求,然後進行檢查,看看是否有新的消息

檢查方法是:計算 待拉取消息請求的偏移量是否小於當前消費隊列最大偏移量,如果條件成立則說明有新消息了,

一旦有消息,PullRequestHoldService 則會調用 notifyMessageArriving ,最終調用 PullMessageProcessor 的 executeRequestWhenWakeup() 方法重新嘗試處理這個消息的請求,也就是再來一次,整個長輪詢的時間默認 30 秒。

簡單的說就是 5 秒會檢查一次消息時候到了,如果到了則調用 processRequest 再處理一次。

這裏是一個定期檢查的流程。除此之外,如果commitLog 有消息,也會執行喚醒的工作,做到準實時。

brocker端的 ReputMessageService 線程,不斷地爲 commitLog 追加數據並分發請求,構建出 ConsumeQueue 和 IndexFile 兩種類型的數據,並且也會有喚醒請求的操作,來彌補每 5s 一次這麼慢的延遲

PUSH 模式的應用開發

下面是 RocketMQ 推模式的一個官方示例:

public static void main(String[] args) throws InterruptedException, MQClientException {
    Tracer tracer = initTracer();

    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1");
    consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(new ConsumeMessageOpenTracingHookImpl(tracer));

    consumer.subscribe("TopicTest", "*");
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

    consumer.setConsumeTimestamp("20181109221800");
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    consumer.start();
    System.out.printf("Consumer Started.%n");
}

消費者會定義一個消息監聽器 MessageListenerConcurrently,並且把這個監聽器註冊到 DefaultMQPushConsumer ,

這個監聽器,最終會註冊到內部 DefaultMQPushConsumerImpl,當內部拉取到消息時,就會使用這個監聽器來處理消息。

消費者消息處理過程

下面用併發消費方式下的同步拉取消息爲例總結一下消費者消息處理過程:

第一類線程 RebalanceService ,根據 topic 的隊列數量和消費者個數做負載均衡,對於分配到queue 產生的 pullRequest拉取請求,並講請求 隊列 pullRequestQueue 中。

第二類線程 PullMessageService ,不斷的pullRequestQueue隊列 中獲取 pullRequest,然後從 broker 拉取消息。具體來說,這裏調用了 DefaultMQPushConsumerImpl 類的 pullMessage 方法; pullMessage 方法調用 PullAPIWrapper 的 pullKernelImpl 方法真正去發送 PULL 請求,並傳入 PullCallback 的 回調函數;拉取到消息後,調用 PullCallback 的 onSuccess 方法處理結果,會把消息放入到 緩存消息的隊列 messageQueue

第三類線程消息消費線程:ConsumeMessageOrderlyService 有序消息消費, 或者 ConsumeMessageConcurrentlyService 並行消息服務。 從messageQueue 拉取消息,進行消費。這裏調用了 ConsumeMessageConcurrentlyService 的 submitConsumeRequest 方法,通過裏面的 ConsumeRequest 線程來處理拉取到的消息;處理消息時調用了消費端定義的消費邏輯,也就是 MessageListenerConcurrently 的 consumeMessage 方法。

Rocketmq 拉模式/PULL 模式

下面是來自官方的一段 拉模式/PULL 模式拉取消息的代碼:

DefaultLitePullConsumer litePullConsumer =
                new DefaultLitePullConsumer("lite_pull_consumer_test");
litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
litePullConsumer.subscribe("TopicTest", "*");
litePullConsumer.start();
try {
    while (running) {
        List<MessageExt> messageExts = litePullConsumer.poll();
        System.out.printf("%s%n", messageExts);
    }
} finally {
    litePullConsumer.shutdown();
}

上面代碼中寫了一個死循環 , 客戶端通過 PULL 模式,不斷的調用poll方法,不停的去拉取消息。

從這段代碼可以看出, 通過拉模式/PULL 模式 的pullRequest 請求,不是Rocketmq 源碼去發出,也不用PullMessageService 線程,這個pullRequest 請求是有 客戶端應用程序自己去發。

Rocketmq 源碼內部,拉模式消費使用的是DefaultMQPullConsumer/DefaultLitePullConsumerImpl,核心邏輯是先拿到需要獲取消息的Topic對應的隊列,然後依次從隊列中拉取可用的消息。拉取了消息後就可以進行處理,處理完了需要更新消息隊列的消費位置。

下面有一個更加生產化的案例

@Test
public void testPullConsumer() throws Exception {
    DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("group1_pull");
    consumer.setNamesrvAddr(this.nameServer);
    String topic = "topic1";
    consumer.start();

    //獲取Topic對應的消息隊列
    Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues(topic);
    int maxNums = 10;//每次拉取消息的最大數量
    while (true) {
        boolean found = false;
        for (MessageQueue messageQueue : messageQueues) {
            long offset = consumer.fetchConsumeOffset(messageQueue, false);
            PullResult pullResult = consumer.pull(messageQueue, "tag8", offset, maxNums);
            switch (pullResult.getPullStatus()) {
                case FOUND:
                    found = true;
                    List<MessageExt> msgs = pullResult.getMsgFoundList();
                    System.out.println(messageQueue.getQueueId() + "收到了消息,數量----" + msgs.size());
                    for (MessageExt msg : msgs) {
                        System.out.println(messageQueue.getQueueId() + "處理消息——" + msg.getMsgId());
                    }
                    long nextOffset = pullResult.getNextBeginOffset();
                    consumer.updateConsumeOffset(messageQueue, nextOffset);
                    break;
                case NO_NEW_MSG:
                    System.out.println("沒有新消息");
                    break;
                case NO_MATCHED_MSG:
                    System.out.println("沒有匹配的消息");
                    break;
                case OFFSET_ILLEGAL:
                    System.err.println("offset錯誤");
                    break;
            }
        }
        if (!found) {//沒有一個隊列中有新消息,則暫停一會。
            TimeUnit.MILLISECONDS.sleep(5000);
        }
    }
}

下面代碼就演示了使用DefaultMQPullConsumer拉取消息進行消費的示例。

核心方法就是調用consumer的pull()拉取消息。

該示例中使用的是同步拉取,即需要等待Broker響應後才能繼續往下執行。如果有需要也可以使用提供了PullCallback的重載方法。同步的pull()返回的是PullResult對象,其中的狀態碼有四種狀態,並且分別對四種狀態進行了不同的處理。

只有狀態爲FOUND才表示拉取到了消息,此時可以進行消費。

消費完了需要調用updateConsumeOffset()更新消息隊列的消費位置,這樣下次通過fetchConsumeOffset()獲取消費位置時才能獲取到正確的位置。

如果有需要,用戶也可以自己管理消息的消費位置。

RocketMQ 中的 PushConsumer 推模式的源碼分析

那 PULL 模式中 poll 函數是怎麼實現的呢?

跟蹤源碼可以看到,消息拉取的時候,DefaultLitePullConsumerImpl工作過程基本與DefaultMQPushConsumer過程相似。

DefaultLitePullConsumerImpl允許設置是否需要自動commit offset(默認自動),並且把拉取到的消息緩存在內存中,Conumser需要主動通過poll從內存中獲取消息,進行業務處理。

DefaultLitePullConsumerImpl 類中的一個方法,首先根據負載均衡服務分配到的 queue分區,啓動 拉取任務

通過定時任務進行消息的拉取

PullTaskImpl拉取到消息後,封裝成ConsumeRequest ,提交的 consumeRequestCache 緩存中

內存緩存consumeRequestCache 類型爲BlockingQueue

消費者代碼中,通過循環,調用poll方法,不停地從 consumeRequestCache 拉取消息進行處理

pull模式,消費者消息處理過程:

總結一下,pull模式,消費者消息處理過程:

  1. 消費者啓動過程中,負載均衡線程 RebalanceService 線程發現 ProcessQueueTable 消費快照發生變化時,啓動消息拉取線程;
  2. 消息拉取線程PullTaskImpl 拉取到消息後,把消息放到 consumeRequestCache,然後進行下一次拉取;
  3. 消費者調用poll方法,不停地從 consumeRequestCache 拉取消息,進行業務處理。

Rocketmq的Push與Pull模式比較

1、Push模式拉取消息,拉取到消息馬上推送lisener進行業務處理。

應用程序對消息的拉取過程參與度不高,可控性不足,僅僅提供消息監聽器的實現。

2、Pull模式自,自主決定如何拉取消息,從什麼位置拉取消息。

應用程序對消息的拉取過程參與度高,由可控性高,可以自主決定何時進行消息拉取,從什麼位置offset拉取消息

上面的流程梳理,涉及到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:

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