整體
分析:
需確保一發一存一消費這些過程均無訊息丟失
利用ACK機制保證每個階段需要執行的操作成功後,再往下一個階段推動(放行)
訊息處理過程:
由上圖分析可知:
訊息丟失,可能發生在三個階段,生產階段、儲存階段、消費階段
如下,為每個階段保證訊息不丟失:
訊息生產階段:
利用MQ的ack確認機制,在try-catch中處理好Broker的返回值,如果返回失敗,則進行重試,若重試次數過多,則進行報警日誌列印,排查解決問題
訊息儲存階段:
刷盤儲存的訊息進行多副本備份處理,從高可用角度取設計中介軟體,搭建叢集;同時,中介軟體也會進行備份,至少兩個節點以上備份成功之後才會給生產者返回ack確認訊息
訊息消費階段:
消費者從消費佇列中拉去訊息後,不是立馬給Broker返回ack確認訊息,而是等待業務程式碼順利執行完成之後,再給Broker返回ack確認訊息
實現:
Producer——>Broker
-
傳送方式
-
同步傳送
- Producer向broker傳送訊息,會阻塞當前執行緒等待broker響應結果
public class SyncProducer { public static void main(String[] args) throws Exception { // 例項化訊息生產者Producer DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // 設定NameServer的地址 producer.setNamesrvAddr("localhost:9876"); // 啟動Producer例項 producer.start(); for (int i = 0; i < 100; i++) { // 建立訊息,並指定Topic,Tag和訊息體 Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); // 傳送訊息到一個Broker SendResult sendResult = producer.send(msg); // 通過sendResult返回訊息是否成功送達 System.out.printf("%s%n", sendResult); } // 如果不再傳送訊息,關閉Producer例項。 producer.shutdown(); } }
-
非同步傳送
- Producer首先構建一個向broker傳送訊息的任務,把該任務提交給執行緒池,等執行完該任務時,回撥使用者自定義的回撥函式,執行處理結果
public class AsyncProducer { public static void main(String[] args) throws Exception { // 例項化訊息生產者Producer DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // 設定NameServer的地址 producer.setNamesrvAddr("localhost:9876"); // 啟動Producer例項 producer.start(); producer.setRetryTimesWhenSendAsyncFailed(0); int messageCount = 100; // 根據訊息數量例項化倒數計時計算器 final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount); for (int i = 0; i < messageCount; i++) { final int index = i; // 建立訊息,並指定Topic,Tag和訊息體 Message msg = new Message("TopicTest", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); // SendCallback接收非同步返回結果的回撥 producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { countDownLatch.countDown(); System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { countDownLatch.countDown(); System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } }); } // 等待5s countDownLatch.await(5, TimeUnit.SECONDS); // 如果不再傳送訊息,關閉Producer例項。 producer.shutdown(); } }
-
Oneway
- Oneway方式只負責傳送請求,不等待應答,Producer只負責把請求發出去,不會處理響應結果
public class OnewayProducer { public static void main(String[] args) throws Exception{ // 例項化訊息生產者Producer DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // 設定NameServer的地址 producer.setNamesrvAddr("localhost:9876"); // 啟動Producer例項 producer.start(); for (int i = 0; i < 100; i++) { // 建立訊息,並指定Topic,Tag和訊息體 Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); // 傳送單向訊息,沒有任何返回結果 producer.sendOneway(msg); } // 如果不再傳送訊息,關閉Producer例項。 producer.shutdown(); } }
-
-
推薦
同步傳送:
- 同步傳送會返回四個狀態碼
- SEND_OK:訊息傳送成功
- FLUSH_DISK_TIMEOUT:訊息傳送成功但是訊息刷盤超時
- FLUSH_SLAVE_TIMEOUT:訊息傳送成功但是訊息同步到 slave 節點時超時
- SLAVE_NOT_AVAILABLE:訊息傳送成功但是 broker 的 slave 節點不可用
- 處理
-
根據返回的狀態碼,進行訊息重試,預設設定為3次,可以通過設定調整
producer.setRetryTimesWhenSendFailed(重試次數);
-
非同步傳送:
- 在onException()方法中處理,如果傳送失敗,則在這裡執行重試
額外問題:
- 如果Broker收到訊息後,就因為某些原因當機了,就算Producer再怎麼重試都是無法解決訊息丟失的問題,該如何處理?
? 利用多主模式,掛了一個,就換一個master繼續訊息傳送
- 同步傳送會返回四個狀態碼
總結:
保證Producer——>Broker訊息不丟失的方案
Broker儲存及備份
-
刷盤
-
同步刷盤
- 訊息寫入記憶體後,立刻呼叫刷盤執行緒進行刷盤
- 如果訊息在約定的時間內未刷盤成功(預設5s),則返回FLUSH_DISK_TIMEOUT,Producer收到後進行重試
-
非同步刷盤(預設)
- 訊息寫入CommitLog時,不會直接寫入磁碟,而是先寫到PageCache快取後返回成功
- 啟用後臺執行緒非同步將訊息刷入磁碟
-
- 高可用
- 多主
- 多個Master節點,防止單主當機,丟失訊息問題
- 主從+雙寫
- 主從的情況下(寫入master成功後立即ACK給Producer),會發生,master——>slave時,主節點Broker當機,同步失敗,從而導致訊息丟失
- 開啟雙寫,只有等master和slave都寫入成功,即雙寫成功後才會ACK給Producer,否則,會觸發Producer的重試機制
- 多主
總結
保證Broker儲存及備份階段,訊息不丟失
Broker——>Consumer
-
訊息確認
- 消費者從Broker中拉去訊息後,不是立馬給Broker返回ack確認訊息,而是等待業務程式碼順利執行完成之後,再給Broker返回ack確認訊息
-
訊息重試
- 訊息消費失敗後,需提供重試訊息的能力,RocketMQ本身提供了重新消費的能力
總結
保證Broker——>Consumer階段,訊息不丟失