RocketMQ進擊(二)一個默認生產者,兩種消費方式,三類普通消息詳解分析


楔子:既然開了車,加了油,那就帶上好心情上路吧。川藏318公路的豁然也好,全美50號公路的孤獨也罷,是奇美,是曠野,是路上的人與風景。

 

1. 在過去的週末

也許週末是個可以用來閒聊這個故事:

1)在一家人看電視的時候,寶寶他媽說給寶寶放動畫片吧,寶寶爸說放哪個呢?寶寶媽問寶寶喜歡看哪個?寶寶說看佩奇,然後寶寶媽跟寶寶爸說放小豬佩奇;等等,這就同步消息,她是在收到接收方返回響應之後再發下一個消息。

2)在看電視的時候,寶寶媽說想喫西瓜,然後又說想喫葡萄,然後又說想喫瓜子,還想喫冰激凌... 於是,寶寶爸就開始去端西瓜,拿瓜子,洗葡萄,再去買冰激凌;等等,這就是異步消息,她並不會等寶寶爸說不或先拿來一樣,再發第二第三次請求,即異步消息不需要等待接收方發回響應,接着發送下個消息,而且接收方也不需要一定按照先後順序完成。

3)在寶寶看小豬佩奇的時候,一般叫他的話他會一動不動,比如寶寶媽喊寶寶他不應,叫他坐在沙發上看也不應,問他那個是誰也不回,簡直充耳不聞;等等,這就是單向消息,其特點就是他不會迴應你,也就是說只發送請求不等待應答。

 

2. 一個默認生產者

DefaultMQProducer 是一個普通模式默認的消息生產者,可以支持發送普通消息和順序消息。

當然還有像 TransactionMQProducer 這樣的事務模式下的消息生產者,這裏不做爲分析對象。

2.1. 源碼分析

public class DefaultMQProducer extends ClientConfig implements MQProducer {

    ......

    public DefaultMQProducer() {
        // 默認構造一個叫 DEFAULT_PRODUCER 的生產者組
        this("DEFAULT_PRODUCER", (RPCHook)null);
    }

    public DefaultMQProducer(String producerGroup, RPCHook rpcHook) {
        // 創建 Topic 時的 topicKey,在測試時可指定 Broker 自增模式
        this.createTopicKey = "AUTO_CREATE_TOPIC_KEY";
        // 默認每個 Topic 中默認有4個 Queue 來存儲消息
        this.defaultTopicQueueNums = 4;
        // 默認發送超時時長 3000ms
        this.sendMsgTimeout = 3000;
        // 默認情況下,當消息體字節數超過4k時,消息會被壓縮(Consumer收到消息會自動解壓縮)
        this.compressMsgBodyOverHowmuch = 4096;
        // 同步發送消息時,消息發送失敗後的最大重試次數
        // RocketMQ 在消息重試機制上有很好的支持,但是重試可能會引起重複消息的問題,這需要在邏輯上進行冪等處理
        this.retryTimesWhenSendFailed = 2;
        // 異步發送時的最大重試次數,類似 retryTimesWhenSendFailed
        this.retryTimesWhenSendAsyncFailed = 2;
        // 如果消息發送成功,但是返回 SendResult != SendStatus.SEND_OK,是否嘗試發往別的 Broker
        this.retryAnotherBrokerWhenNotStoreOK = false;
        // 默認最大消息長度:4M,當消息長度超過限制時,RocketMQ 會自動拋出異常
        this.maxMessageSize = 4194304;
        // 生產者組
        this.producerGroup = producerGroup;
        // 構建一個默認生產者
        this.defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    }

    ......

2.2 實現示例

往下看,詳見三種普通消息的實現和代碼分析。有點小長,但很簡單。

 

3. 兩種消費方式

在 RocketMQ 中,消費者 Consumer 分爲兩類:MQPullConsumer(DefaultMQPullConsumer 爲其實現類) 和 MQPushConsumer(DefaultMQPushConsumer 爲其實現類)。但二者其本質都是 pull 模式,即 Consumer輪詢從 Broker 拉取消息。

3.1. 拉取式消費(Pull Consumer)

在 pull 方式中,需要應用自己實現拉取消息的過程,首先通過消費的 Topic 拿到 MessageQueue 集合,並遍歷MessageQueue 集合,然後針對每個 MessageQueue 批量拉取消息。取完一次後,記錄 MessageQueue 下一次要取的起始 offset,取完後再換下一個 MessageQueue。Pull 方式中 Consumer 與 Broker 建立的是短連接。

3.2. 推動式消費(Push Consumer)

在 push 方式中,Consumer 把輪詢的過程封裝了。當應用註冊 MessageListener 後,Broker 接收到消息時,會自動回調MessageListener 的 consumeMessage() 方法,在 Consumer 端執行消費。對於應用來說,這個過程好像是消息自動推送過來的。Push 方式中 Consumer 與 Broker 建立了長連接。

 

4. 三類普通消息

使用 RocketMQ 發送三種類型的消息:同步消息、異步消息和單向消息。其中前兩種消息是可靠的,因爲會有發送是否成功的應答。

前置依賴:開發前,我們需要加入相關 pom 依賴:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.3.0</version>
</dependency>

 

4.1 可靠同步發送

原理簡解:同步發送是指消息發送方發出數據後,會在收到接收方發回響應之後才發下一個數據包的通訊方式。

應用場景:這種可靠同步方式發送應用場景非常廣泛,例如重要通知郵件、報名短信通知、營銷短信系統等。

4.1.1 源碼與示例

同步消息生產者(Producer)

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;

import java.util.Date;

/**
 * 可靠同步發送 - 生產者
 * <p>
 * 原理
 * 同步發送是指消息發送方發出數據後,會在收到接收方發回響應之後才發下一個數據包的通訊方式。
 * <p>
 * 應用場景
 * 此種方式應用場景非常廣泛,例如重要通知郵件、報名短信通知、營銷短信系統等。
 */
public class SimpleSyncMqProducer {

    // 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_SYNC";

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

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

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

        // 循環發送MQ測試消息
        String content = "";
        for (int i = 0; i < 5; i++) {
            content = "【MQ測試消息】可靠同步發送 " + i;

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

            // 發送消息。send 方法默認使用的是同步發送方式,有返回結果
            // 發送消息到一個 Broker
            SendResult sendResult = producer.send(message);

            // 同步發送消息,只要不拋異常就是成功
            if (sendResult != null) {
                // 消息發送成功
                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()));
            } else {
                System.out.println(new Date() + " Send MQ message failed! Topic: " + message.getTopic());
            }
        }

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

同步消息消費者(Consumer)【Push消費方式】

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;

/**
 * 可靠同步發送 - 消費者(Push模式)
 */
public class SimpleMqPushSyncConsumer {
    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    // 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_SYNC";

    public static void main(String[] args) throws Exception {
        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-simple-sync-push");

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

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

        // 設置一個Listener,主要進行消息的邏輯處理
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {

                list.forEach(msg->{
                    System.out.printf("Thread: %s, Topic: %s, Tags: %s, MsgId: %s, Message: %s %n",
                            Thread.currentThread().getName(),
                            msg.getTopic(),
                            msg.getTags(),
                            msg.getMsgId(),
                            new String(msg.getBody()));
                });

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

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

同步消息消費者(Consumer)【Pull消費方式】

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

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.common.message.MessageQueue;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 可靠同步發送 - 消費者(Pull模式)
 */
public class SimpleMqPullSyncConsumer {

    // 記錄每個 MessageQueue 的消費位點 offset,可以持久化到 DB 或緩存 Redis,這裏作爲演示就保存在程序中
    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

    // 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_SYNC";

    public static void main(String[] args) throws Exception {
        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("meiwei-consumer-simple-sync-pull");
        // 同樣也要設置 NameServer 地址,須要與提供者的地址列表保持一致
        consumer.setNamesrvAddr("127.0.0.1:9876");

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

        // 獲取該MessageQueue的消費位點
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues(MQ_CONFIG_TOPIC);

        // 遍歷MessageQueue,獲取Message
        for (MessageQueue mq : mqs) {
            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);

                    // 記錄offset
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    switch (pullResult.getPullStatus()) {
                        // 拉取到消息
                        case FOUND:
                            break;
                        // 沒有匹配的消息
                        case NO_MATCHED_MSG:
                            break;
                        // 暫時沒有新消息
                        case NO_NEW_MSG:
                            break SINGLE_MQ;
                        // offset非法
                        case OFFSET_ILLEGAL:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }

    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);
    }
}

4.1.2 測試及結果

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

同步消息消費者(Consumer)消費結果:

4.1.3 源碼分析

public class DefaultMQProducerImpl implements MQProducerInner {

    ......

    // 默認使用的是同步發送方式
    public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, (SendCallback)null, timeout);
    }

    ......
public enum CommunicationMode {
    SYNC,
    ASYNC,
    ONEWAY;

    private CommunicationMode() {
    }
}

可能看到,發送消息的 send 方法默認使用的是同步發送方式,有返回結果。

 

4.2. 可靠異步發送

原理簡解:異步發送是指發送方發出數據後,不等接收方發回響應,接着發送下個數據包的通訊方式。消息隊列 MQ 的異步發送,需要用戶實現異步發送回調接口(SendCallback)。消息發送方在發送了一條消息後,不需要等待服務器響應即可返回,進行第二條消息發送。發送方通過回調接口接收服務器響應,並對響應結果進行處理。

應用場景:異步發送一般用於鏈路耗時較長,對 RT 響應時間較爲敏感的業務場景,即發送端不能容忍長時間地等待 Broker 的響應。例如用戶視頻上傳後通知啓動轉碼服務,轉碼完成後通知推送轉碼結果等。

4.2.1 源碼與示例

異步消息生產者(Producer)

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

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

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 可靠異步發送 - 生產者
 * <p>
 * 原理
 * 異步發送是指發送方發出數據後,不等接收方發回響應,接着發送下個數據包的通訊方式。
 * 消息隊列 MQ 的異步發送,需要用戶實現異步發送回調接口(SendCallback)。
 * 消息發送方在發送了一條消息後,不需要等待服務器響應即可返回,進行第二條消息發送。
 * 發送方通過回調接口接收服務器響應,並對響應結果進行處理。
 */
public class SimpleAsyncMqProducer {

    // 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_ASYNC = "PID_MEIWEI_SMS_ASYNC";

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

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

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

        // 設置重試次數,默認情況下是2次重試
        producer.setRetryTimesWhenSendFailed(0);

        int msgCount = 3;
        // 實例化一個倒計數器,count 指定計數個數
        final CountDownLatch countDownLatch = new CountDownLatch(msgCount);

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

                // Message Body 可以是任何二進制形式的數據,消息隊列不做任何干預,需要 Producer 與 Consumer 協商好一致的序列化和反序列化方式
                Message message = new Message(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_ASYNC, content.getBytes(RemotingHelper.DEFAULT_CHARSET));
                producer.send(message, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        // 計數減一
                        countDownLatch.countDown();
                        // 消息發送成功
                        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()));
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        // 計數減一
                        countDownLatch.countDown();
                        // 消息發送失敗
                        System.out.printf("%-10d Exception %s %n", index, throwable);
                        throwable.printStackTrace();
                    }
                });
            } catch (Exception e) {
                // 消息發送失敗
                System.out.printf("%-10d Exception %s %n", i, e);
                e.printStackTrace();
            }
        }

        // 等待,當計數減到0時,所有線程並行執行
        countDownLatch.await(5, TimeUnit.SECONDS);

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

異步消息消費者(Consumer)【Push消費方式】

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.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * 可靠異步發送 - 消費者(Push模式)
 */
public class SimpleMqPushAsyncConsumer {
    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    // 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_ASYNC";

    public static void main(String[] args) throws Exception {
        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-simple-async");

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

        // 設置 consumer 的消費策略
        // CONSUME_FROM_LAST_OFFSET 默認策略,從該隊列最尾開始消費,即跳過歷史消息
        // CONSUME_FROM_FIRST_OFFSET 從隊列最開始開始消費,即歷史消息(還儲存在broker的)全部消費一遍
        // CONSUME_FROM_TIMESTAMP 從某個時間點開始消費,和setConsumeTimestamp()配合使用,默認是半個小時以前
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

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

        // 設置一個Listener,主要進行消息的邏輯處理
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println(Thread.currentThread().getName() + " Receive new message: " + list);
                for (MessageExt msg : list) {
                    System.out.printf("Thread: %s, Topic: %s, Tags: %s, MsgId: %s, Message: %s %n",
                            Thread.currentThread().getName(),
                            msg.getTopic(),
                            msg.getTags(),
                            msg.getMsgId(),
                            new String(msg.getBody()));
                }

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

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

4.2.2 測試及結果

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

異步消息消費者(Consumer)消費結果:

 

4.3. 單向發送

原理簡解:單向(Oneway)發送特點爲發送方只負責發送消息,不等待服務器迴應且沒有回調函數觸發,即只發送請求不等待應答。此方式發送消息的過程耗時非常短,一般在微秒級別。

應用場景:適用於某些耗時非常短,但對可靠性要求並不高的場景,例如日誌收集。

4.3.1 源碼與示例

單向消息生產者(Producer)

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

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

/**
 * 單向(Oneway)發送 - 生產者
 * <p>
 * 原理
 * 單向(Oneway)發送特點爲發送方只負責發送消息,不等待服務器迴應且沒有回調函數觸發,即只發送請求不等待應答。
 * 此方式發送消息的過程耗時非常短,一般在微秒級別。
 * <p>
 * 應用場景
 * 適用於某些耗時非常短,但對可靠性要求並不高的場景,例如日誌收集。
 */
public class SimpleOnewayMqProducer {

    // 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_ONEWAY = "PID_MEIWEI_SMS_ONEWAY";

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

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

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

        // 循環發送MQ測試消息
        String content = "";
        for (int i = 0; i < 5; i++) {
            content = "【MQ測試消息】單向消息發送 " + i;

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

            // 單向發送模式沒有返回值,就是說只管發不管發送投遞是否成功
            producer.sendOneway(message);

            // 消息發送成功
            System.out.printf("Send MQ message success! Topic: %s, Tag: %s, Message: %s %n",
                    message.getTopic(), message.getTags(), new String(message.getBody()));
        }

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

單向消息消費者(Consumer)【Push消費方式】

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.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * 單向(Oneway)發送 - 消費者(Push模式)
 */
public class SimpleMqPushOnewayConsumer {
    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    // Message 所屬的 Topic 一級分類,須要與提供者的頻道保持一致才能消費到消息內容
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    private static final String MQ_CONFIG_TAG_ONEWAY = "PID_MEIWEI_SMS_ONEWAY";

    public static void main(String[] args) throws Exception {
        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-oneway-push");

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

        // 設置 consumer 的消費策略
        // CONSUME_FROM_LAST_OFFSET 默認策略,從該隊列最尾開始消費,即跳過歷史消息
        // CONSUME_FROM_FIRST_OFFSET 從隊列最開始開始消費,即歷史消息(還儲存在broker的)全部消費一遍
        // CONSUME_FROM_TIMESTAMP 從某個時間點開始消費,和setConsumeTimestamp()配合使用,默認是半個小時以前
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

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

        // 設置一個Listener,主要進行消息的邏輯處理
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println(Thread.currentThread().getName() + " Receive new message: " + list);
                for (MessageExt msg : list) {
                    System.out.printf("Thread: %s, Topic: %s, Tags: %s, MsgId: %s, Message: %s %n",
                            Thread.currentThread().getName(),
                            msg.getTopic(),
                            msg.getTags(),
                            msg.getMsgId(),
                            new String(msg.getBody()));
                }

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

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

4.3.2 測試及結果

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

單向消息消費者(Consumer)消費結果:

 

5. 發送方式對比

如下概括了三種發送方式的特點和主要區別:

發送方式 發送 TPS 發送結果反饋 可靠性
同步發送 不丟失
異步發送 不丟失
單向發送 最快 可能丟失

 


參考資料:
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'

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