【消息中間件】--- RocketMQ消費者簡介(集羣、廣播消費,推模式,拉模式)

本文對應源碼地址:https://github.com/nieandsun/rocketmq-study
rocketmq官網:https://rocketmq.apache.org/docs/quick-start/
rocketmq github託管地址(這裏直接給出的是中文docs地址):https://github.com/apache/rocketmq/tree/master/docs/cn


1 集羣消費/廣播消費概念簡介

消息隊列 RocketMQ 是基於發佈/訂閱模型的消息系統。 消息的訂閱方訂閱關注的 Topic, 以獲取並消費消息。 由於訂閱方應用一般是分佈式系統,以集羣方式部署有多臺機器。 因此消息隊列 RocketMQ 約定以下概念:


1.1 集羣

集羣: 使用相同 Group ID 的訂閱者屬於同一個集羣。 同一個集羣下的訂閱者消費邏輯必須完全一致(包括 Tag 的使用) , 這些訂閱者在邏輯上可以認爲是一個消費節點。


1.2 集羣消費(Clustering)+ 使用場景&注意事項

集羣消費: 當使用集羣消費模式時, 消息隊列 RocketMQ 認爲任意一條消息只需要被集羣內的任意一個消費者處理即可 —> 工作原理如下圖所示:
在這裏插入圖片描述


適用場景&注意事項:

  • (1)消費端集羣化部署, 每條消息只需要被處理一次;
  • (2)由於消費進度在服務端維護, 可靠性更高。
  • (3)Topic + Tag下的消息可以保證肯定會被整個集羣至少消費一次 ;
  • (4)不保證每一次失敗重投的消息路由到同一臺機器上, 因此處理消息時不應該做任何確定性假設。
  • (5)集羣中的每個消費者消費的消息肯定不會是同一條消息,因爲實際上在集羣模式下
    • 每一個queue都只能被一個消費者消費
    • 但是每一個消費者都可以消費多個queue

因此,如下圖所示,每個消費者消費的肯定不是同一個消息。
在這裏插入圖片描述


1.3 廣播消費(Broadcasting)+ 使用場景&注意事項

廣播消費: 當使用廣播消費模式時, 消息隊列 RocketMQ 會將每條消息推送給集羣內所有註冊過的客戶端, 保證消息至少被每臺機器消費一次 —> 作原理如下圖所示:
在這裏插入圖片描述


相比於集羣模式,廣播模式的特點爲: 每個消費者都會消費所訂閱的Topic + Tag下的所有queue中的所有消息。
適用場景&注意事項:

  • (1)廣播消費模式下不支持順序消息。
  • (2)廣播消費模式下不支持重置消費位點。
  • (3)每條消息都需要被相同邏輯的多臺機器處理。
  • (4)廣播模式下, 消息隊列 RocketMQ 保證每條消息至少被每臺客戶端消費一次, 但是並不會對消費失敗的消息進行失敗重投, 因此業務方需要關注消費失敗的情況。
  • (5)廣播模式下, 客戶端每一次重啓都會從最新消息消費。 客戶端在被停止期間發送至服務端的消息將會被自 動跳過, 請謹慎選擇
  • (6)廣播模式下, 每條消息都會被大量的客戶端重複處理, 因此推薦儘可能使用集羣模式。
  • (7)目前僅 Java 客戶端支持廣播模式。
  • (8)廣播模式下服務端不維護消費進度, 所以消息隊列 RocketMQ 控制檯不支持消息堆積查詢、 消息堆積報警和訂閱關係查詢功能。
  • (9)消費進度在客戶端維護, 出現消息重複消費的概率稍大於集羣模式。

1.4 簡單聊一聊RocketMQ的設計理念 — 至少一次


1.4.1 RocketMQ保證至少消費一次的原理簡介

上面介紹到:

  • 在集羣模式下,RocketMQ 可以保證Topic + Tag下的消息可以肯定會被整個集羣至少消費一次
  • 在廣播模式下,RocketMQ 可以保證至少被每臺機器消費一次

其實現原理是什麼呢?官網(https://github.com/apache/rocketmq/blob/master/docs/cn/features.md)介紹如下。

這裏其實和mysql實現事務的原理非常類似,我就不過多言語了。
在這裏插入圖片描述


1.4.2 至少一次不是有且僅有一次 — 若冪等性要求高,需在業務層面進行去重處理

這裏並不是我咬文嚼字,因爲官網上(https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md)有對其進行明確地說明:
在這裏插入圖片描述
可以想象,RocketMQ之所以這樣做肯定是爲了效率而不去做更多的判斷 。
給我們的提示: 如果項目中的冪等性確實要求比較高 —> 肯定要在業務層面進行去重處理。


2 集羣消費測試

測試代碼如下:

/**
 * 消費者-推模式-集羣
 */
public class PushClusterConsumerA {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        //實例化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");

        //設置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");//指定NameServer地址

        //訂閱一個或者多個Topic,以及Tag來過濾需要消費的消息
        consumer.subscribe("Topic-NRSC", "*");

        //設置消費模式爲廣播模式
        //consumer.setMessageModel(MessageModel.BROADCASTING);

        //每次從最後一次消費的地址
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        //註冊回調實現類來處理從broker拉取回來的消息
        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("ConsumerPartOrder Started.%n");
    }
}

測試結果:
先啓動三個同groupName的消費者組成一個集羣,他們都訂閱主題 — Topic-NRSC, 然後啓動生產者向broker發送10條消息,三個消費者消費的消息分別如下:

  • ConsumerA

在這裏插入圖片描述

  • ConsumerB

在這裏插入圖片描述

  • ConsumerC

在這裏插入圖片描述

再回過頭去看1中所講的集羣消費的概念,相信你肯定就都懂了。


3 廣播消費測試

測試代碼如下:
只需要將2中設置消費模式爲廣播模式:下注釋掉的那一行代碼打開就可以了。

測試結果:
先啓動三個以廣播模式接收消息的消費者,他們都訂閱主題 — Topic-NRSC, 然後啓動生產者向broker發送10條消息,三個消費者消費的消息分別如下:

  • ConsumerA、ConsumerB和ConsumerC 結果一樣,如下:

在這裏插入圖片描述

再回過頭去看1中所講的廣播消費的概念,相信你肯定也都懂了。


4 工作經驗分享

(1)說實話我們的項目中其實沒用過廣播模式
(2)假使你的業務裏真的需要使用廣播模式,其實我覺得你也可以在開發中先用集羣模式,爲什麼這樣說呢?

有心的讀者可能看到了,在1中介紹集羣消費和廣播消費時,分別有兩句話:

  • 集羣模式: 由於消費進度在服務端維護, 可靠性更高。
  • 廣播模式: 服務端不維護消費進度, 所以消息隊列 RocketMQ 控制檯不支持消息堆積查詢、 消息堆積報警和訂閱關係查詢功能。

上面這兩句話的具體意思是啥呢?
其實很簡單,假設消費者還沒開啓,此時生產者向broker發送了10條Topic 爲 XXX 的消息,那麼:

  • 在集羣模式下

    • 如果此時開啓一個訂閱了 XXX 消息的消費者肯定會接收到這10條中的某一條或某幾條
    • 更爽的是假如你把這個進程停了,只要換個消費者groupName再重新啓動進程就又可以再消費這10條中的某一條或某幾條消息了
    • 這有啥好處呢??? —> 當然是非常方便在debug模式下調試+ 優化你的代碼了。
  • 在廣播模式下

    • 對不起,這時候你將收不到任何消息,因爲廣播模式下,服務端不維護消費進度----> 客戶端每一次重啓都會從最新消息消費。 客戶端在被停止期間發送至服務端的消息將會被自動跳過, 因此請謹慎選擇

5 拉取式消費(Pull Consumer)簡介

其實2 中的代碼就是推動式消費(Push Consumer) ,在該模式下Broker收到數據後會主動推送給消費端,該消費模式一般實時性較高 —》 據說在底層其實它還是使用了拉取模式,只是消費者和broker之間維持了一個長連接。


接下來看一下拉取模式如何實現消息消費,示例代碼如下。

/***
 * 拉模式
 */
public class PullConsumer {
    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pullconsumer");
        consumer.setNamesrvAddr("localhost:9876");
        //consumer.setBrokerSuspendMaxTimeMillis(1000);

        System.out.println("ms:" + consumer.getBrokerSuspendMaxTimeMillis());
        consumer.start();

        //1.獲取MessageQueues並遍歷(一個Topic包括多個MessageQueue)
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("Topic-NRSC");
        for (MessageQueue mq : mqs) {
            System.out.println("queueID:" + mq.getQueueId());
            //獲取偏移量
            long Offset = consumer.fetchConsumeOffset(mq, true);

            System.out.printf("Consume from the queue: %s%n", mq);
            SINGLE_MQ:
            while (true) {
                try {
                    PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    System.out.printf("%s%n", pullResult);
                    //2.維護Offsetstore(這裏存入一個Map)
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

                    //3.根據不同的消息狀態做不同的處理
                    switch (pullResult.getPullStatus()) {
                        case FOUND: //獲取到消息
                            for (int i = 0; i < pullResult.getMsgFoundList().size(); i++) {
                                System.out.printf("%s%n", new String(pullResult.getMsgFoundList().get(i).getBody()));
                            }
                            break;
                        case NO_MATCHED_MSG: //沒有匹配的消息
                            break;
                        case NO_NEW_MSG:  //沒有新消息
                            break SINGLE_MQ;
                        case OFFSET_ILLEGAL: //非法偏移量
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        consumer.shutdown();
    }

    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSE_TABLE.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        OFFSE_TABLE.put(mq, offset);
    }
}

說實話代碼量相比於推模式還是挺多的,而且感覺並沒有推模式好理解。我在項目裏其實也沒用過這種模式。
有興趣的自己clone下來本文對應的源碼研究研究吧:https://github.com/nieandsun/rocketmq-study, 這裏我就不過多展開了。


  • 簡單看一下上訴代碼的執行結果:

在這裏插入圖片描述


end !!! — 2020/05/18 02:07

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