RocketMQ進擊(五)集羣消費模式與廣播消費模式


楔子:新一天的旅程,掠過天空海灣,越過低谷高山,躍過深淵淺灘,在天南地北,走兩江四岸,與日月星辰,看錦繡山河。

 

1. 兩種消費模式

RocketMQ 有兩種消費模式:

集羣消費模式:CLUSTERING,可以理解爲同組公共消費。公共資源我拿了你就沒有。即同一 Topic 下,一個 ConsumerGroup 下如果有多個實例(可以是多個進程,或者多個機器),那麼這些實例會均攤消費這些消息,但我消費了這條消費你就不會再消費。消費者默認是集羣消費方式適用於大部分消息業務。

廣播消費模式:BROADCASTING,可以理解爲同組各自消費。即同一 Topic 下,同一消息會被多個實例各自都消費一次。所以,廣播消費模式中的 ConsumerGroup 概念沒有太大的意義。這適用於一些分發消息的場景。

 

1.1. 集羣消費模式

1.1.1 生產者

package com.meiwei.service.mq.tcp.producer;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class ClusteringMqProducer {

    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";

    // Tag 爲 Message 所屬的二級分類,比如初中可分爲初一、初二、初三;高中可分爲高一、高二、高三
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_CLUSTERING";

    public static void main(String[] args) throws Exception {
        // 聲明並實例化一個 producer 生產者來產生消息
        // 需要一個 producer group 名字作爲構造方法的參數
        DefaultMQProducer producer = new DefaultMQProducer("meiwei-producer-clustering");

        // 指定 NameServer 地址列表,多個nameServer地址用半角分號隔開。此處應改爲實際 NameServer 地址
        // NameServer 的地址必須有,但也可以通過啓動參數指定、環境變量指定的方式設置,不一定要寫死在代碼裏
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 在發送MQ消息前,必須調用 start 方法來啓動 Producer,只需調用一次即可
        producer.start();

        // 循環發送MQ測試消息
        String content = "";
        for (int i = 0; i < 10; i++) {
            // 配置容災機制,防止當前消息異常時阻斷髮送流程
            try {
                content = "【MQ測試消息】測試消息 " + i;

                // Message Body 可以是任何二進制形式的數據,消息隊列不做任何干預,需要 Producer 與 Consumer 協商好一致的序列化和反序列化方式
                Message message = new Message(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH, "KEY" + i, content.getBytes(RemotingHelper.DEFAULT_CHARSET));

                // 發送消息
                SendResult sendResult = producer.send(message);

                // 日誌打印
                System.out.printf("Send MQ message success! Topic: %s,Tag: %s, msgId: %s, Message: %s %n",
                        message.getTopic(), message.getTags(), sendResult.getMsgId(), new String(message.getBody()));
            } catch (Exception e) {
                // 消息發送失敗
                System.out.printf("%-10d Exception %s %n", i, e);
                e.printStackTrace();
            }
        }

        // 在發送完消息之後,銷燬 Producer 對象。如果不銷燬也沒有問題
        producer.shutdown();
    }
}

1.1.2 集羣消費模式消費

package com.meiwei.service.mq.tcp.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * 集羣消息模式
 */
public class ClusteringMqConsumer {

    // Message 所屬的 Topic 一級分類,須要與提供者的頻道保持一致才能消費到消息內容
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_CLUSTERING";

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

        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-clustering");

        // 同樣也要設置 NameServer 地址,須要與提供者的地址列表保持一致
        consumer.setNamesrvAddr("127.0.0.1:9876");

        // 設置 consumer 所訂閱的 Topic 和 Tag,*代表全部的 Tag
        consumer.subscribe(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH);

        // 註冊消息監聽者
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach(mq->{
                    System.out.printf("Thread: %s, Host: %s, Key: %s, QueueId: %s, Topic: %s, Tags: %s, Message: %s",
                            Thread.currentThread().getName(),
                            mq.getBornHost(),
                            mq.getKeys(),
                            mq.getQueueId(),
                            mq.getTopic(),
                            mq.getTags(),
                            new String(mq.getBody()));
                    System.out.println();
                });

                // 返回消費狀態
                // CONSUME_SUCCESS 消費成功
                // RECONSUME_LATER 消費失敗,需要稍後重新消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 調用 start() 方法啓動 consumer
        consumer.start();
        System.out.println("Clustering Consumer Started.");
    }
}

注:將此消費者拷貝一份爲 ClusteringMqConsumer2,其它不動,以便測試。

 

1.1.3 測試及結果

啓動集羣消費者1、集羣消費者2 和 消息生產者。

消息生產者(Producer)發送結果:

集羣消費模式 消費者1 消費結果:

集羣消費模式 消費者2 消費結果:

可以看到兩個消費者是在多線程下的分攤消費效果。

 

1.2. 廣播消費模式

1.2.1 生產者

package com.meiwei.service.mq.tcp.producer;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class BroadcastingMqProducer {

    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";

    // Tag 爲 Message 所屬的二級分類,比如初中可分爲初一、初二、初三;高中可分爲高一、高二、高三
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_BROADCASTING";

    public static void main(String[] args) throws Exception {
        // 聲明並實例化一個 producer 生產者來產生消息
        // 需要一個 producer group 名字作爲構造方法的參數
        DefaultMQProducer producer = new DefaultMQProducer("meiwei-producer-broadcasting");

        // 指定 NameServer 地址列表,多個nameServer地址用半角分號隔開。此處應改爲實際 NameServer 地址
        // NameServer 的地址必須有,但也可以通過啓動參數指定、環境變量指定的方式設置,不一定要寫死在代碼裏
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 在發送MQ消息前,必須調用 start 方法來啓動 Producer,只需調用一次即可
        producer.start();

        // 循環發送MQ測試消息
        String content = "";
        for (int i = 0; i < 10; i++) {
            // 配置容災機制,防止當前消息異常時阻斷髮送流程
            try {
                content = "【MQ測試消息】測試消息 " + i;

                // Message Body 可以是任何二進制形式的數據,消息隊列不做任何干預,需要 Producer 與 Consumer 協商好一致的序列化和反序列化方式
                Message message = new Message(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH, "KEY" + i, content.getBytes(RemotingHelper.DEFAULT_CHARSET));

                // 發送消息
                SendResult sendResult = producer.send(message);

                // 日誌打印
                System.out.printf("Send MQ message success! Topic: %s,Tag: %s, msgId: %s, Message: %s %n",
                        message.getTopic(), message.getTags(), sendResult.getMsgId(), new String(message.getBody()));
            } catch (Exception e) {
                // 消息發送失敗
                System.out.printf("%-10d Exception %s %n", i, e);
                e.printStackTrace();
            }
        }

        // 在發送完消息之後,銷燬 Producer 對象。如果不銷燬也沒有問題
        producer.shutdown();
    }
}

1.2.2. 廣播消費模式消費

package com.meiwei.service.mq.tcp.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.util.List;

/**
 * 廣播消費模式(Broadcasting)
 * 廣播消費模式下,相同Consumer Group的每個Consumer實例都接收全量的消息。
 */
public class BroadcastingMqConsumer {

    // Message 所屬的 Topic 一級分類,須要與提供者的頻道保持一致才能消費到消息內容
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_BROADCASTING";
    private static final String MQ_CONFIG_TAG_PUSH_OTHER = "PID_MEIWEI_SMS_OTHER";

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

        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-broadcasting");

        // 同樣也要設置 NameServer 地址,須要與提供者的地址列表保持一致
        consumer.setNamesrvAddr("127.0.0.1:9876");

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

        // 設置 consumer 所訂閱的 Topic 和 Tag,*代表全部的 Tag
        consumer.subscribe(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH + " || " + MQ_CONFIG_TAG_PUSH_OTHER);

        // 註冊消息監聽者
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach(mq->{
                    System.out.printf("Thread: %s, Host: %s, Key: %s, QueueId: %s, Topic: %s, Tags: %s, Message: %s",
                            Thread.currentThread().getName(),
                            mq.getBornHost(),
                            mq.getKeys(),
                            mq.getQueueId(),
                            mq.getTopic(),
                            mq.getTags(),
                            new String(mq.getBody()));
                    System.out.println();
                });

                // 返回消費狀態
                // CONSUME_SUCCESS 消費成功
                // RECONSUME_LATER 消費失敗,需要稍後重新消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 調用 start() 方法啓動 consumer
        consumer.start();
        System.out.println("Broadcasting Consumer Started.");
    }
}

注:

  1. 將此消費者拷貝一份爲 BroadcastingMqConsumer2,其它不動。
  2. 再拷貝一份爲 BroadcastingMqConsumer3,修改其 ConsumerGroup 參數爲 meiwei-consumer-broadcasting3。

這樣得到 Consumer 和 Consumer2 同組,又與 Consumer3 不同組的消費者實例,以便測試。

 

1.2.3. 測試及結果

分別啓動廣播消費者1、廣播消費者2 和 消息生產者。

消息生產者(Producer)發送結果:

廣播消費模式 消息者1(BroadcastingMqConsumer)消費結果:

廣播消費模式 消息者2(BroadcastingMqConsumer2)消費結果:

可以看到兩個消息消費者都收到了同樣的消息。

再啓動廣播消費者3(BroadcastingMqConsumer3),再次執行消息生產者,廣播消費者3同樣消費了所有消息。

 

2. 源碼分析

MessageModel:

package org.apache.rocketmq.common.protocol.heartbeat;

public enum MessageModel {
    BROADCASTING("BROADCASTING"),
    CLUSTERING("CLUSTERING");

    private String modeCN;

    private MessageModel(String modeCN) {
        this.modeCN = modeCN;
    }

    public String getModeCN() {
        return this.modeCN;
    }
}

DefaultMQPushConsumer:

    public DefaultMQPushConsumer(String consumerGroup, RPCHook rpcHook, AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
        this.messageModel = MessageModel.CLUSTERING;
        this.consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
        this.consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - 1800000L);
        this.subscription = new HashMap();
        this.consumeThreadMin = 20;
        this.consumeThreadMax = 64;
        this.adjustThreadPoolNumsThreshold = 100000L;
        this.consumeConcurrentlyMaxSpan = 2000;
        this.pullThresholdForQueue = 1000;
        this.pullThresholdSizeForQueue = 100;
        this.pullThresholdForTopic = -1;
        this.pullThresholdSizeForTopic = -1;
        this.pullInterval = 0L;
        this.consumeMessageBatchMaxSize = 1;
        this.pullBatchSize = 32;
        this.postSubscriptionWhenPull = false;
        this.unitMode = false;
        this.maxReconsumeTimes = -1;
        this.suspendCurrentQueueTimeMillis = 1000L;
        this.consumeTimeout = 15L;
        this.consumerGroup = consumerGroup;
        this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
        this.defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
    }

默認的 DefaultMQPushConsumer 內部定義了很多默認值,比如默認爲集羣消費方式,線程最小默認20,最大默認64,批量下拉消息默認32,默認一次消費一條消息等等。


參考資料:
RocketMQ 官網:http://rocketmq.apache.org/docs/motivation/
阿里雲消息隊列 MQ:https://help.aliyun.com/document_detail/29532.html
阿里巴巴中間件團隊:http://jm.taobao.org/2016/11/29/apache-rocketmq-incubation/


RocketMQ進擊物語:
RocketMQ進擊(零)RocketMQ這個大水池子
RocketMQ進擊(一)Windows環境下安裝部署Apache RocketMQ
RocketMQ進擊(二)一個默認生產者,兩種消費方式,三類普通消息詳解分析
RocketMQ進擊(三)順序消息與高速公路收費站
RocketMQ進擊(四)定時消息(延時隊列)
RocketMQ進擊(五)集羣消費模式與廣播消費模式
RocketMQ進擊(六)磕一磕RocketMQ的事務消息
RocketMQ進擊(七)盤一盤RocketMQ的重試機制
RocketMQ進擊(八)RocketMQ的日誌收集Logappender
RocketMQ異常:RocketMQ順序消息收不到或者只能收到一部分消息
RocketMQ異常:Unrecognized VM option 'MetaspaceSize=128m'

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