RocketMQ的可靠性傳輸

Zhao發表於2022-03-10

整體

分析:

需確保一發一存一消費這些過程均無訊息丟失

利用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階段,訊息不丟失

最終方案:

相關文章