一、順序消息的實現
1.全局順序消息
要保證全局順序消息,需要先把 Topic 的讀寫隊列數設置爲 1,
然後 Producer 和 Consumer 的併發設置也要是1。
在發送端,要做到 把同一業務 ID 的消息發送到同一個 Message Queue
(使用 MessageQueueSelector類)
2.部分順序消息
在消費過程中,要做到從 同一個 Message Queue 讀取的消息不被併發處理
(使用 MessageListenerOrderly類)
具體實現方式:
在 MessageListenerOrderly 的實現中,爲每個 Consumer Queue 加個鎖,
消費每個消息前,需要先獲得這個消息對應的 Consumer Queue 所對應的鎖,
這樣保證了同一時間,同一個 Consumer Queue 的消息不被併發消 費,
但不同 Consumer Queue 的消息可以併發處理 。
具體參考代碼如下:
package org.apache.rocketmq.example.ordermessage;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
二、消息重複問題
消息重複一般情況下不會發生,但是如果消息量大,網絡有波動,消息重複就是個大概率事件。 主要造成的原因是:
Producer有個setRetryTimesWhenSendFailed函數, 設置在同步方式下自動重試的次數,默認值是 2,這樣當第一次發送消息時, Broker端接收到了消息但是沒有正確返回發送成功的狀態,就造成了消息重複。
解決辦法:
- 保證消費邏輯的幕等性(多次調用和一次調用效果相同)
- 維護一個巳消費消息的記錄,消費前查詢這個消息是否被消費過 (只能在入庫時,手動寫代碼去重)
三、消息優先級
RocketMQ 是個先入先出的隊列,不支持消息級別或者 Topic 級別的優先級 ,只能通過間接方式。
情景1:
同一Topic下某一消息需要被及時處理,可另外開闢topic
情景2:
一個訂單處理系統,接收從 100家快遞門店過來的請求,把這些請求通過 Producer 寫人 RocketMQ ;
訂單處理程序通過 Consumer 從隊列裏讀取消 息並處理,每天最多處理 1 萬單 。
如果這 100 個快遞門店中某幾個門店訂單量大增,
比如門店一接了個大客戶,一個上午就發出 2萬單消息請求,
這樣其他的 99 家門店可能被迫等待門店一的 2 萬單處理完,
也就是兩天後訂單才能被處理,顯然很不公平 。
解決:
創建一個 Topic, 設置 Topic 的 MessageQueue 數 量 超過 100 個,
Producer根據訂單的門店號,把每個門店的訂單寫人 一 個 MessageQueue。
DefaultMQPushConsumer默認是採用循環的方式逐個讀取一個 Topic 的 所有
MessageQueue,這樣如果某家門店訂單 量 大增,這家門店對應的
MessageQueue 消息數增多,等待時間增長,但不會造成其他家門店等待時間增長。
情景3:
一個應用程序同時處理 TypeA、 TypeB、 TypeC 三類消息 。
TypeA 處於第 一優先級,要確保只要有 TypeA消息,必須優先處理;
TypeB處於第二優先級;
TypeC 處於第三 優先級 。
解決:
對這種要求,或者邏輯更復雜的要求,需要自己編碼實現優先級控制,
如果上述的三類消息在一個 Topic 裏,可以使 用 PullConsumer,
自主控制 Messag巳Queue 的遍歷,以及消息的讀取;
四、各種故障對消息隊列的影響
故障清單:
1) Broker正常關閉,啓動;
2) Broker異常 Crash,然後啓動;
3) OS Crash,重啓;
4 )機器斷電,但能馬上恢復供電;
5 )磁盤損壞;
6) CPU、 主板、內存等關鍵設備損壞 。
分析:
1 )情況 屬於可控的軟件 問題,內存中的數據不會丟失 。
若重啓過程中有持續運行的 Consumer, Master機器出故障後,
Consumer會自動重連到對應的 Slave 機器,不會有消息丟失和偏差 。
當 Master 角色的機器 重啓 以 後, Consumer又會重新連接到 Master機器
若有持續運行的 Producer,一臺Master 出故障後,
Producer只能向 Topic下其他的 Master機器發送消息,
如果Producer採用同步發送方式,不會有消息丟失 。
2、3、4)情況屬於軟件故障,內存的數據可能丟失,所以刷盤策略不同,造成的影響也不同,
如果 Master、 Slave都配置成 SYNC_FLUSH,可以達到和第 1 種情況相同的效果 。
5、6 )情況屬於硬件故障 ,發生第 5、6 種情況的故障,原有機器的磁盤數據可能會丟失。 如果Master和Slave機器間配置成同步複製方式,某一臺機器發生 5 或 6 的故障,也可以達到消息不丟失的效果 。 機器間是異步複製,兩次 Sync間的消息會丟失。
常規思路:
總的來說,當設置成:
1 )多 Master,每個 Master 帶有 Slave;
2 )主從之間設置成 SYNC_MASTER;
3 ) Producer 用同步方式寫;
4 )刷盤策略設置成 SYNC FLUSH。
就可以消除單點依賴,即使某臺機器出現極端故障也不會丟消息 。