楔子:既然開了車,加了油,那就帶上好心情上路吧。川藏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'