本文對應源碼地址: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