一、 關鍵特性
1 消息發送和消費
1)消息發送者步驟分析:
- 創建消息生產者producer,并制定生產者組名
- 指定NameServer地址
- 啟動producer
- 創建消息對象,指定主題Topic、Tag和消息體
- 發送消息
- 關閉生產者producer
2)消息消費者步驟分析:
- 創建消費者consumer,制定消費者組名
- 指定NameServer地址
- 訂閱主題Topic和Tag
- 設置回調函數,處理消息
- 啟動消費者consumer
2 消息類型
使用RocketMQ可以發送普通消息、順序消息、事務消息,順序消息能實現有序消費,事務消息可以解決分布式事務實現數據最終一致。
1)普通消息
消息隊列 MQ 提供三種方式來發送普通消息:
- 可靠同步發送
同步發送是指消息發送方發出數據后,會在收到接收方發回響應之后才發下一個數據包的通訊方式。這種可靠的消息發送方式使用的比較廣泛,比如:重要的消息通知,短信通知。
public class SyncProducer {
public static void main(String[] args) throws Exception {
//- 創建消息生產者producer,并制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//- 指定NameServer地址
producer.setNamesrvAddr("192.168.217.130:9876");
//- 啟動producer
producer.start();
//- 創建消息對象,指定主題Topic、Tag和消息體
Message message = new Message("base","Tag1","keys_1",("hello").getBytes());
//- 發送消息
SendResult result = producer.send(message);
//發送狀態
SendStatus sendStatus = result.getSendStatus();
//消息id
String msgId = result.getMsgId();
//消息接受隊列id
int queueId = result.getMessageQueue().getQueueId();
TimeUnit.SECONDS.sleep(3);
System.out.println("發送狀態"+result+",消息id"+msgId+",隊列"+queueId);
//- 關閉生產者producer
producer.shutdown();
}
}
- 可靠異步發送
異步發送是指發送方發出數據后,不等接收方發回響應,接著發送下個數據包的通訊方式,發送方通過回調接口接收服務器響應,并對響應結果進行處理。異步消息通常用在對響應時間敏感的業務場景,即發送端不能容忍長時間地等待Broker的響應。
public class AsyncProducer {
public static void main(String[] args) throws Exception {
//- 創建消息生產者producer,并制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//- 指定NameServer地址
producer.setNamesrvAddr("192.168.217.130:9876");
//- 啟動producer
producer.start();
//- 創建消息對象,指定主題Topic、Tag和消息體
for (int i = 0; i < 3; i++) {
Message message = new Message("base","Tag2",("hello"+i).getBytes());
//- 發送異步消息
producer.send(message, new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println("發送成功:"+sendResult);
}
public void onException(Throwable throwable) {
System.out.println("發送異常:"+throwable);
}
});
TimeUnit.SECONDS.sleep(3);
}
//- 關閉生產者producer
producer.shutdown();
}
}
- 單向發送消息
這種方式注意用在不特別關心發送結果的場景,例如日志發送。
public class OnewayProducer {
public static void main(String[] args) throws Exception {
//- 創建消息生產者producer,并制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//- 指定NameServer地址
producer.setNamesrvAddr("192.168.217.130:9876");
//- 啟動producer
producer.start();
//- 創建消息對象,指定主題Topic、Tag和消息體
for (int i = 0; i < 3; i++) {
Message message = new Message("base","Tag3",("hello"+i).getBytes());
//- 發送單向消息
producer.sendOneway(message);
TimeUnit.SECONDS.sleep(3);
}
//- 關閉生產者producer
producer.shutdown();
}
}
- 編寫消息消費者消費消息( 啟動時需要先啟動消費者監聽)
public class Consumer {
public static void main(String[] args) throws MQClientException {
//- 創建消費者consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//- 指定NameServer地址
consumer.setNamesrvAddr("192.168.217.130:9876");
//- 訂閱主題Topic和Tag
consumer.subscribe("base","*");
//- 設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list) {
System.out.println(new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//- 啟動消費者consumer
consumer.start();
}
}
RocketMQ 常見異常處理
2) 延時消息
消息在發送到消息隊列 MQ 服務端后并不會立馬投遞,而是根據消息中的屬性延遲固定時間后才投遞給消費者。但是RocketMQ不支持任意時間精度,僅支持特定的 level,例如定時 5s, 10s, 1m 等。其中,level=0 級表示不延時,level=1 表示 1 級延時,level=2 表示 2 級延時,以此類推。
在服務器端(rocketmq-broker端)的屬性配置文件中加入以下行:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
描述了各級別與延時時間的對應映射關系。
? 這個配置項配置了從1級開始,各級延時的時間,可以修改這個指定級別的延時時間;
? 時間單位支持:s、m、h、d,分別表示秒、分、時、天;
? 默認值就是上面聲明的,可手工調整;
? 默認值已夠用,不建議修改這個值。
public class DelayProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("192.168.217.130:9876");
producer.start();
//延時10s
Message message = new Message("base","Tag1","keys_1",("hello").getBytes());
message.setDelayTimeLevel(3);
producer.send(message);
producer.shutdown();
}
}
如果你使用阿里云服務器,可以使用阿里封裝的api,它支持定時消息和延時消息,可以適應更多場景。
詳細介紹和代碼示例
3) 順序消息
消息有序指的是可以按照消息的發送順序來消費(FIFO)。RocketMQ可以嚴格的保證消息有序,可以分為分區有序或者全局有序。
詳細介紹
4) 事務消息
消息隊列 MQ 提供類似 X/Open XA 的分布式事務功能,通過消息隊列 MQ 事務消息能達到分布式事務的最終一致。上圖說明了事務消息的大致流程:正常事務消息的發送和提交、事務消息的補償流程。
事務消息發送及提交:
①發送消息(half消息);
②服務端響應消息寫入結果;
③根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行);
④根據本地事務狀態執行Commit或Rollback(Commit操作生成消息索引,消息對消費者可見)。事務消息的補償流程:
①對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”;
②Producer收到回查消息,檢查回查消息對應的本地事務的狀態。
③根據本地事務狀態,重新Commit或RollBack
其中,補償階段用于解決消息Commit或Rollback發生超時或者失敗的情況。
- 事務消息狀態:
事務消息共有三種狀態:提交狀態、回滾狀態、中間狀態:
①TransactionStatus.CommitTransaction:提交事務,它允許消費者消費此消息。
②TransactionStatus.RollbackTransaction:回滾事務,它代表該消息將被刪除,不允許被消費。
③TransactionStatus.Unkonwn:中間狀態,它代表需要檢查消息隊列來確定消息狀態。
詳細介紹和代碼示例
消息類型對比:
Topic的消息類型 | 是否支持事務消息 | 是否支持定時/延時消息 | 性能 |
---|---|---|---|
無序消息(普通、事務、定時/延時消息) | 是 | 是 | 最高 |
分區順序消息 | 否 | 否 | 高 |
全局順序消息 | 否 | 否 | 一般 |
發送方式對比:
消息類型 | 是否支持同步發送 | 是否支持異步發送 | 是否支持單向發送 |
---|---|---|---|
無序消息(普通、事務、定時/延時消息) | 是 | 是 | 最高 |
分區順序消息 | 是 | 否 | 否 |
全局順序消息 | 是 | 否 | 否 |
3 批量消息
批量發送消息能顯著提高傳遞消息的性能,限制是這些消息應該具有相同的topic,相同的waitStoreMsgOK,而且不能是延時消息。此外,這一批量消息的總大小不應超過1MB。如果超過,需要把消息分割。
不超過1M,直接producer.send(msg)就可以了。
超過IM,消息分割代碼示例
4 消息消費方式
(1)負載均衡模式
消費者默認采用負載均衡方式,多個消費者共同消費隊列消息,每個消費者處理的消息不同。
(2)廣播模式
消費者采用廣播的方式消費消息,每個消費者消費的消息都是相同的。
Producer負載均衡
Producer端,每個實例在發消息的時候,默認會輪詢所有的message queue發送,以達 到讓消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息 就發送到不同的broker下,如下圖:
圖中箭頭線條上的標號代表順序,發布方會把第一條消息發送至 Queue 0,然后第二條 消息發送至 Queue 1,以此類推。
Consumer負載均衡
1)集群模式
在集群消費模式下,每條消息只需要投遞到訂閱這個topic的Consumer Group下的一個 實例即可。RocketMQ采用主動拉取的方式拉取并消費消息,在拉取的時候需要明確指定 拉取哪一條message queue。 而每當實例的數量有變更,都會觸發一次所有實例的負載均衡,這時候會按照queue的 數量和實例的數量平均分配queue給每個實例。 默認的分配算法是AllocateMessageQueueAveragely,如下圖:
還有另外一種平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分攤 每一條queue,只是以環狀輪流分queue的形式,如下圖:
需要注意的是,集群模式下,queue都是只允許分配只一個實例,這是由于如果多個實 例同時消費一個queue的消息,由于拉取哪些消息是consumer主動控制的,那樣會導致 同一個消息在不同的實例下被消費多次,所以算法上都是一個queue只分給一個 consumer實例,一個consumer實例可以允許同時分到不同的queue。 通過增加consumer實例去分攤queue的消費,可以起到水平擴展的消費能力的作用。而 有實例下線的時候,會重新觸發負載均衡,這時候原來分配到的queue將分配到其他實 例上繼續消費。 但是如consumer實例的數量比message queue的總數量還多的話,多出來的 consumer實例將無法分到queue,也就無法消費到消息,也就無法起到分攤負載的作用了。所以需要控制讓queue的總數量大于等于consumer的數量。
2)廣播模式
由于廣播模式下要求一條消息需要投遞到一個消費組下面所有的消費者實例,所以也就 沒有消息被分攤消費的說法。 在實現上,就是在consumer分配queue的時候,所有consumer都分到所 有的queue。
《深入理解RocketMQ》- MQ消息的投遞機制
5 簡單消息過濾
1) Tag過濾
RocketMQ 的消息過濾方式有別于其他消息中間件,是在訂閱時,再做過濾,先來看下 Consume Queue 的存儲結構。
(1)在 Broker 端進行 Message Tag 比對,先遍歷 Consume Queue,如果存儲的 Message Tag 與訂閱的 Message Tag 不符合,則跳過,繼續比對下一個,符合則傳輸給 Consumer。注意:Message Tag 是字符串形式,Consume Queue 中存儲的是其對應的 hashcode,比對時也是比對 hashcode。
(2)Consumer 收到過濾后的消息后,同樣也要執行在 Broker 端的操作,但是比對的是真實的 Message Tag 字 符串,而不是 Hashcode。
為什么過濾要這樣做?
(1)Message Tag 存儲 Hashcode,是為了在 Consume Queue 定長方式存儲,節約空間。
(2)過濾過程中不會訪問 Commit Log 數據,可以保證堆積情況下也能高效過濾。
(3) 即使存在 Hash 沖突,也可以在 Consumer 端進行修正,保證萬無一失。
簡單消息過濾通過指定多個 Tag 來過濾消息,過濾動作在服務器進行。如:
consumer.subscribe("TopicTest1", "TagA || TagC || TagD");
以上方式對于復雜的場景可能不起作用,因為一個消息只能有一個tag。這種情況下,可以使用SQL表達式篩選消息。
2) SQL語法過濾
consumer.subscribe("TopicTest",MessageSelector
.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" + "and (a is not null and a between 0 3)"));
注意:只有使用push模式的消費者此案使用SQL92標準的sql語句。
6 消息重試
1)順序消息的重試
對于順序消息,當消費者消費消息失敗后,消息隊列 RocketMQ 會自動不斷進行消息重 試(每次間隔時間為 1 秒),這時,應用會出現消息消費被阻塞的情況。因此,在使用順序消息時,務必保證應用能夠及時監控并處理消費失敗的情況,避免阻塞現象的發生。
2) 無序消息的重試
對于無序消息(普通、定時、延時、事務消息),當消費者消費消息失敗時,您可以通過設置返回狀態達到消息重試的結果。
無序消息的重試只針對集群消費方式生效;廣播方式不提供失敗重試特性,即消費失敗后,失敗消息不再重試,繼續消費新的消息。
3)死信隊列
當一條消息初次消費失敗,消息隊列 RocketMQ 會自動進行消息重試;達到最大重試次 數后,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 RocketMQ 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中。 在消息隊列 RocketMQ 中,這種正常情況下無法被消費的消息稱為死信消息(Dead-Letter Message),存儲死信消息的特殊隊列稱為死信隊列(Dead-Letter Queue)。
7 消費冪等
二、消息存儲
分布式隊列因為有高可靠性的要求,所以數據要進行持久化存儲。
流程:
(1) 消息生成者發送消息;
(2) MQ收到消息,將消息進行持久化,在存儲中新增一條記錄 ;
(3) 返回ACK給生產者;
(4) MQ push 消息給對應的消費者,然后等待消費者返回ACK;
(5) 如果消息消費者在指定時間內成功返回ack,那么MQ認為消息消費成功,在存儲中 刪除消息,即執行第6步;如果MQ在指定時間內沒有收到ACK,則認為消息消費失 敗,會嘗試重新push消息,重復執行4、5、6步驟
(6) MQ刪除消息。
1 存儲介質
(1) 關系型數據庫DB
Apache下開源的另外一款MQ—ActiveMQ(默認采用的KahaDB做消息存儲)可選用 JDBC的方式來做消息持久化,通過簡單的xml配置信息即可實現JDBC消息存儲。由于, 普通關系型數據庫(如Mysql)在單表數據量達到千萬級別的情況下,其IO讀寫性能往往 會出現瓶頸。在可靠性方面,該種方案非常依賴DB,如果一旦DB出現故障,則MQ的消 息就無法落盤存儲會導致線上故障。
(2) 文件系統
目前業界較為常用的幾款產品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盤 至所部署虛擬機/物理機的文件系統來做持久化(刷盤一般可以分為異步刷盤和同步刷盤兩種模式)。消息刷盤為消息存儲提供了一種高效率、高可靠性和高性能的數據 持久化方式。除非部署MQ機器本身或是本地磁盤掛了,否則一般是不會出現無法持 久化的故障問題。
2 性能對比
文件系統>關系型數據庫DB
3 消息的存儲和發送
1)消息存儲
磁盤如果使用得當,磁盤的速度完全可以匹配上網絡 的數據傳輸速度。目前的高性能磁 盤,順序寫速度可以達到600MB/s, 超過了一般網卡的傳輸速度。但是磁盤隨機寫的速 度只有大概100KB/s,和順序寫的性能相差6000倍!因為有如此巨大的速度差別,好的 消息隊列系統會比普通的消息隊列系統速度快多個數量級。RocketMQ的消息用順序寫, 保證了消息存儲的速度。
2)消息發送
Linux操作系統分為【用戶態】和【內核態】,文件操作、網絡操作需要涉及這兩種形態 的切換,免不了進行數據復制。 一臺服務器 把本機磁盤文件的內容發送到客戶端,一般分為兩個步驟:
1)read;讀取本地文件內容;
2)write;將讀取的內容通過網絡發送出去。
這兩個看似簡單的操作,實際進行了4 次數據復制,分別是:
1. 從磁盤復制數據到內核態內存;
2. 從內核態內存復 制到用戶態內存;
3. 然后從用戶態 內存復制到網絡驅動的內核態內存;
4. 最后是從網絡驅動的內核態內存復 制到網卡中進行傳輸。
Consumer 消費消息過程,使用了零拷貝,零拷貝包含以下兩種方式
- 使用 mmap + write 方式
優點:即使頻繁調用,使用小塊文件傳輸,效率也很高
缺點:不能很好的利用 DMA 方式,會比 sendfile 多消耗 CPU,內存安全性控制復雜,需要避免 JVM Crash問題。 - 使用 sendfile 方式
優點:可以利用 DMA 方式,消耗 CPU 較少,大塊文件傳輸效率高,無內存安全新問題。
缺點:小塊文件效率低于 mmap 方式,只能是 BIO 方式傳輸,不能使用 NIO。
RocketMQ 選擇了第一種方式,mmap+write 方式,因為有小塊數據傳輸的需求,效果會比 sendfile 更好。
關于 Zero Copy 的更詳細介紹,請參考以下文章
http://www.linuxjournal.com/article/6345
通過使用mmap的方式,可以省去向用戶態的內存復制,提高速度。這種機制在Java中是 通過MappedByteBuffer實現的 RocketMQ充分利用了上述特性,提高消息存盤和網絡發送 的速度。
這里需要注意的是,采用MappedByteBuffer這種內存映射的方式有幾個限制,其 中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是為何RocketMQ 默認設置單個CommitLog日志數據文件為1G的原因了。
MQ消息最終一致性解決方案
4 消息存儲結構
RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成 的,消息真正的物 理存儲文件是CommitLog,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文 件,存儲的是指向物理存儲的地址。每 個Topic下的每個Message Queue都有一個對應 的ConsumeQueue文件。
CommitLog:存儲消息的元數據
ConsumerQueue:存儲消息在CommitLog的索引
IndexFile:為了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程
5 刷盤機制
RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電后恢復, 又可以讓存儲的消息 量超出內存的限制。RocketMQ為了提高性能,會盡可能地保證磁盤的順序寫。消息在通 過Producer寫入RocketMQ的時 候,有兩種寫磁盤方式,分布式同步刷盤和異步刷盤。
1)同步刷盤
在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE 后,立刻通知刷盤線程刷盤, 然后等待刷盤完成,刷盤線程執行完成后喚醒等待的線 程,返回消息寫 成功的狀態。
2)異步刷盤
在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,吞 吐量大;當內存里的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。
3)配置
同步刷盤還是異步刷盤,都是通過Broker配置文件里的flushDiskType 參數設置的, 這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一個。
三、高可用性機制
RocketMQ分布式集群是通過Master和Slave的配合達到高可用性的。
Master和Slave的區別:在Broker的配置文件中,參數 brokerId的值為0表明這個Broker 是Master,大于0表明這個Broker是 Slave,同時brokerRole參數也會說明這個Broker 是Master還是Slave。 Master角色的Broker支持讀和寫,Slave角色的Broker僅支持讀,也就是 Producer只能 和Master角色的Broker連接寫入消息;Consumer可以連接 Master角色的Broker,也可 以連接Slave角色的Broker來讀取消息。
1 消息消費高可用
在Consumer的配置文件中,并不需要設置是從Master讀還是從Slave 讀,當Master不 可用或者繁忙的時候,Consumer會被自動切換到從Slave 讀。有了自動切換Consumer 這種機制,當一個Master角色的機器出現故障后,Consumer仍然可以從Slave讀取消 息,不影響Consumer程序。這就達到了消費端的高可用性。
2 消息發送高可用
在創建Topic的時候,把Topic的多個Message Queue創建在多個Broker組上(相同 Broker名稱,不同 brokerId的機器組成一個Broker組),這樣當一個Broker組的 Master不可 用后,其他組的Master仍然可用,Producer仍然可以發送消息。 RocketMQ目前還不支持把Slave自動轉成Master,如果機器資源不足, 需要把Slave轉 成Master,則要手動停止Slave角色Broker,更改配置文 件,用新的配置文件啟動 Broker。
3 消息主從復制
如果一個Broker組有Master和Slave,消息需要從Master復制到Slave 上,有同步和異步兩種復制方式。
1)同步復制
同步復制方式是等Master和Slave均寫 成功后才反饋給客戶端寫成功狀態;
在同步復制方式下,如果Master出故障, Slave上有全部的備份數據,容易恢復,但是同 步復制會增大數據寫入 延遲,降低系統吞吐量。
2)異步復制
異步復制方式是只要Master寫成功 即可反饋給客戶端寫成功狀態。在異步復制方式下,系統擁有較低的延遲和較高的吞吐量,但是如果Master出了故障, 有些數據因為沒有被寫 入Slave,有可能會丟失;
3)配置
同步復制和異步復制是通過Broker配置文件里的brokerRole參數進行設置的,這個參數 可以被設置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三個值中的一個。
4) 總結
實際應用中要結合業務場景,合理設置刷盤方式和主從復制方式, 尤其是SYNC_FLUSH 方式,由于頻繁地觸發磁盤寫動作,會明顯降低 性能。通常情況下,應該把Master和 Save配置成ASYNC_FLUSH的刷盤 方式,主從之間配置成SYNC_MASTER的復制方式,這 樣即使有一臺 機器出故障,仍然能保證數據不丟,是個不錯的選擇。