RocketMQ 入門學習筆記

前言

本文主要側重點在 java 的客戶端,具體的如何搭建 RocketMQ 服務端不在本文討論範圍內

一、一些概念

1.1、rocketMQ 的組成

在這裏插入圖片描述

  • name server 用來保存 Broker 相關 Topic 等元信息並給 Producer ,提供 Consumer 查找 Broker 信息。
  • producer 消息生產者,負責產生消息
  • consumer 消息消費者,負責消費消息
    • push consumer:Consumer的一種,應用通常向Consumer對象註冊一個Listener接口,一旦收到消息,Consumer對象立刻回調Listener接口方法
    • pull consumer :Consumer的一種,應用通常主動調用Consumer的拉消息方法從Broker拉消息,主動權由應用控制
  • Producer Group:多個 producer 可以同屬於一個 Producer Group
  • Consumer Group:多個consumer 可以同屬於一個 Consumer Group,同個分組中的消費者一定要訂閱相同的topic
  • broker:消息中轉角色,負責存儲消息

1.2、消費方式

廣播消費和集羣消費

  • 廣播消費:一條消息被多個Consumer消費,即使這些 Consumer屬於同一個 Consumer Group,消息也會被Consumer Group 中的每個 Consumer都消費一次。
  • 集羣消費:一個Consumer Group中的Consumer實例平均分攤消費消息。例如某個Topic有9條消息,其中一個Consumer Group有3個 實例(可能是3個進程或3臺機器),那麼每個實例只消費其中的3條消息。

1.3、消費者發送消息的方式

  • 同步可靠:客戶端阻塞,等待服務端響應結果,成功後,客戶端繼續運行
  • 異步可靠:客戶端不阻塞,設置一個鉤子,待服務端響應成功後,調用鉤子
  • 單向:不關心服務端的響應結果,直接發給服務端就完事了

1.4、消息類型

  • 普通消息
  • 順序消息
  • 延時消息
  • 批量消息

二、四種消息

2.1、普通消息的發送和消費

public class Test {

    public static String PRODUCER_GROUP_NAME = "producer_group_1";
    public static String CONSUMER_GROUP_NAME = "consumer_group_1";
    public static String NAME_SERVER_HOST = "127.0.0.1:9876";
    public static String TOPIC_NAME = "TEST_TOPIC";

    public static void main(String[] args) throws Exception {
        // 創建生產者實例
        DefaultMQProducer producer = new DefaultMQProducer();
        // 設置name server 信息
        producer.setNamesrvAddr(NAME_SERVER_HOST);
        // 設置 producer group
        producer.setProducerGroup(PRODUCER_GROUP_NAME);

        // 啓動生產者實例
        producer.start();

        // 啓動兩個消費者
        openConsumer("xfz1");
        openConsumer("xfz2");
        // 循環發送消息
        for (int i = 1; i < 40; i++) {
            Message msg = new Message();
            msg.setTopic(TOPIC_NAME);
            msg.setTags("tag1");
            msg.setBody(("消息:" + i).getBytes());

            // 使用生產者實例發送消息
            producer.send(msg);
        }

        // 銷燬生產者
        producer.shutdown();

    }

    public static void openConsumer(String name) {
        // 開啓一個消費者線程
        new Thread(() -> {
            // 創建消費者實例
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
            // 設置name server
            consumer.setNamesrvAddr(NAME_SERVER_HOST);
            // 設置消費者組
            consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
            // 設置消費者name
            consumer.setInstanceName(name);
            // 訂閱 topic
            try {
                consumer.subscribe(TOPIC_NAME, "tag1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 設置鉤子
            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for(MessageExt msg :msgs){
                    System.out.println("消費者:"+ consumer.getInstanceName()+" 消費了消息:"+new String(msg.getBody()));
                }
                // 睡眠500ms
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            // 啓動consumer
            try {
                consumer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

注意點:

  • DefaultMQProducer、DefaultMQPushConsumer 都繼承了 ClientConfig ,就是說,這兩個類可以直接使用和設置 rocketMQ 的客戶端配置,在上面的例子中,我只設置了name server 的host
  • 發送消息時需要設置消息的 topic、tag、body
  • DefaultMQPushConsumer 在有消息時,會調用我們設置好的鉤子,即 MessageListenerConcurrently 的 consumeMessage方法
  • Consumer 要設置實例名稱

程序結果:
在這裏插入圖片描述

2.2、有序消息

2.2.1、rocketMQ 是如何做到有序的?

要了解這個問題,需要明白

  • producer 和 message、 broker、queue 的關係
  • consumer 和 message、broker、queue 的關係

producer 和 message、 broker、queue 的關係

broker 是消息 queue 的提供者,而 broker 支持分佈式,即集羣部署。當一個生產者實例發出了一條消息時,需要先選擇一個 broker 的一個 queue。
在這裏插入圖片描述

consumer 和 message、broker、queue 的關係

而消費時,多個queue是同時拉取提交到消費者。
在這裏插入圖片描述

rocketMQ 如何做到有序

我們知道,同一個 queue 是可以做到 FIFO 的,如果我們想辦法把所有消息投遞到同一個 broker 的同一個 queue,自然消息就變成有序的了。但是,只是使用同一個 broker 的同一個 queue 就可以了嗎?並不是,因爲同一個 queue 的消息有可能被多個消費者的消費,而每個消費者又有可能有多個線程在消費,爲了保持 queue 中的消息順序消費,我們還需要保證同一時刻,只有一個消費者的一個消費線程在消費 queue。

消息置入同一個 queue 方案:rocketMQ 支持在生產者投遞消息時,設置隊列的選擇算法,即含有 MessageQueueSelector 的 send 方法。

保證同一時刻,只有一個消費者的一個消費線程在消費:使用 DefaultMQPushConsumer 的 MessageListenerOrderly 這個監聽器處理消息。

2.2.2、全局有序

所有消息全部進入同一個隊列,消費時,只有一個消費者的一個線程可以消費這個隊列的消息
在這裏插入圖片描述

生產者設置 MessageQueueSelector,選擇 queue

                // 使用生產者實例發送消息
                try {
//                    producer.send(msg);
                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            return mqs.get(0);
                        }
                    },null);
                } catch (Exception e) {
                    e.printStackTrace();
                }

這裏,select 返回的 queue,即爲消息即將投遞的 queue,我們直接把第一個 queue 返回,相當於所有的消息直接投遞到第一個 queue

消費者順序消費

            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for (MessageExt msg : msgs) {
                    System.out.println("消費者:" + consumer.getInstanceName() + " 消費了消息:" + new String(msg.getBody()) + " 消費線程:" + Thread.currentThread().getName()+" 消息所在隊列id:"+msg.getQueueId());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });

全局有序demo代碼

public class Test3 {

    public static String PRODUCER_GROUP_NAME = "producer_group_1";
    public static String CONSUMER_GROUP_NAME = "consumer_group_1";
    public static String NAME_SERVER_HOST = "127.0.0.1:9876";
    public static String TOPIC_NAME = "TEST_TOPIC";

    public static void main(String[] args) throws Exception {

        // 啓動生產者
        createProducer("prd1");

        Thread.sleep(5000);
        // 啓動兩個消費者
        createConsumer("xfz1");
        createConsumer("xfz2");
        createConsumer("xfz3");

    }

    public static void createConsumer(String name) {
        // 開啓一個消費者線程
        new Thread(() -> {
            // 創建消費者實例
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
            // 設置name server
            consumer.setNamesrvAddr(NAME_SERVER_HOST);
            // 設置消費者組
            consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
            // 設置消費者name
            consumer.setInstanceName(name);

            consumer.setConsumeThreadMax(1);
            consumer.setConsumeThreadMin(1);
            // 訂閱 topic
            try {
                consumer.subscribe(TOPIC_NAME, "tag1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 設置鉤子
            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for (MessageExt msg : msgs) {
                    System.out.println("消費者:" + consumer.getInstanceName() + " 消費了消息:" + new String(msg.getBody()) + " 消費線程:" + Thread.currentThread().getName()+" 消息所在隊列id:"+msg.getQueueId());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            // 啓動consumer
            try {
                consumer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();
    }

    public static void createProducer(String name) {
        new Thread(() -> {
            // 創建生產者實例
            DefaultMQProducer producer = new DefaultMQProducer();
            // 設置name server 信息
            producer.setNamesrvAddr(NAME_SERVER_HOST);
            // 設置 producer group
            producer.setProducerGroup(PRODUCER_GROUP_NAME);
            // 設置生產者名稱
            producer.setInstanceName(name);

            // 啓動生產者實例
            try {
                producer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 循環發送消息
            for (int i = 1; i < 10000; i++) {
                Message msg = new Message();
                msg.setTopic(TOPIC_NAME);
                msg.setTags("tag1");
                msg.setBody(("消息" + i).getBytes());
                // 使用生產者實例發送消息
                try {
//                    producer.send(msg);
                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                            if (((Integer)arg & 1) == 1) {
                                // 奇數
                                return mqs.get(0);
                            }else {
                                // 偶數
                                return mqs.get(1);
                            }

                        }
                    }, i);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }).start();
    }
}

2.2.3、局部有序

全局有序是局部有序的一種特殊情況,全局有序是所有的消息全部進入同一個 queue,這樣所有的消息都會有序;而局部有序是某個場景下的一部分消息(比如id是偶數的消息,id小於1000的消息等等場景)進入同一個 queue,這樣就可以保證在同一個場景下(同一個 queue)中,消息是有序的。
下面寫一個demo,將 id 是奇數的消息和 id 是偶數的消息分別放入 queue,並消費

生產者設置 MessageQueueSelector,選擇 queue

奇數放入第一個 queue,偶數放入第二個 queue。

                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                            if ((num & 1) == 1) {
                                // 奇數
                                return mqs.get(0);
                            }else {
                                // 偶數
                                return mqs.get(1);
                            }

                        }
                    }, null);

2.3、延時消息

RocketMQ 支持發送延遲消息,但不支持任意時間的延遲消息的設置,僅支持內置預設值的延遲時間間隔的延遲消息。

預設值的延遲時間間隔爲:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h

message 設置延時

msg.setDelayTimeLevel(3);

2.4、批量消息

批量發送消息可提高傳遞小消息的性能。同時也需要滿足以下特徵

  • 批量消息要求必要具有同一topic、相同消息配置
  • 不支持延時消息
  • 建議一個批量消息最好不要超過1MB大小

代碼:

       List<Message> msgs = new ArrayList<>();
        for (int i = 1; i < 40; i++) {
            Message msg = new Message();
            msg.setTopic(TOPIC_NAME);
            msg.setTags("tag1");
            msg.setBody(("消息:" + i).getBytes());
            msgs.add(msg);

        }
        // 使用生產者實例發送消息
        producer.send(msgs);

三、最佳實踐

3.1、消息發送最佳實踐

3.1.1、一個應用儘可能用一個Topic

一個應用儘可能用一個Topic,消息子類型用tags來標識,tags可以由應用自由設置。只有發送消息設置了tags,消費方在訂閱消息時,纔可以利用tags在broker做消息過濾。

3.1.2、打印合適的消息日誌

消息發送成功或者失敗,要打印消息日誌,務必要打印sendresult和key字段。

3.1.3、發送成功的幾個狀態

send消息方法,只要不拋異常,就代表發送成功。但是發送成功會有多個狀態,在sendResult裏定義。

  • SEND_OK
    消息發送成功
  • FLUSH_DISK_TIMEOUT
    消息發送成功,但是服務器刷盤超時,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失
  • FLUSH_SLAVE_TIMEOUT
    消息發送成功,但是服務器同步到Slave時超時,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失
  • SLAVE_NOT_AVAILABLE
    消息發送成功,但是此時slave不可用,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失

3.1.4、消息發送的重試

3.1.4.1、只有同步發送纔會進行失敗重試,重新投遞消息

rocketMQ 有3種類型的發送

  • 同步
    投遞消息的線程阻塞,等待獲取投遞消息的結果,對應 send(Message msg)
  • 異步
    投遞消息的線程不阻塞,等投遞結果返回時,調用設置好的鉤子,對應 send(Message msg, SendCallback sendCallback)
  • oneway
    投遞消息的線程不阻塞,不關心投遞結果,對應 sendOneway(Message msg)

只有同步發送時,纔會在消息投遞失敗時,進行重試

3.2、消息消費最佳實踐

3.2.1、一個 consumer group 只訂閱一個 topic

rocketMQ 文檔中明確指出,強烈建議一個 consumer group 只訂閱一個 topic。

但是,能不能多個 consumer group 訂閱同一個 topic 呢?答案是可以的。
多個 consumer group 訂閱同一個 topic 時,Topic的一條消息會廣播給所有訂閱的ConsumerGroup,就是每個ConsumerGroup都會收,但是在一個ConsumerGroup內部給個Consumer是負載消費消息的,翻譯一下就是:一條消息在一個group內只會被一個Consumer消費

3.2.2、消費端需要做到冪等

rocketmq 無法避免消息的重複消費,消費端需要做冪等處理。處理方式:

  • 1、增加唯一性約束,如果出現重複插入,則直接報錯,不進行處理即可
  • 2、加鎖(悲觀鎖或者樂觀鎖)
    比如,如果希望更新一個字段,這個時候可以加悲觀鎖:
    直接在jvm 層面加鎖或者加分佈式鎖控制,或者在mysql 做select for update
    也可以加樂觀鎖:使用MVCC
發佈了52 篇原創文章 · 獲贊 7 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章