RocketMQ系列之消息重試+重複消費

前言

上節我們介紹了RMQ的幾大模塊以及每個模塊的作用,前面也介紹了RMQ的訂閱消費案例,今天我們來看一個RMQ的消息重試機制和重複消費的問題,瞭解這2點有助於我們更好更合理的處理消息消費的異常情況。

 

 

爲什麼會出現消息重試?

因爲RMQ的消息都是通過網絡傳輸的,通過網絡傳輸就難免會受網絡環境的影響,各種可能存在的情況,可能導致生產者Producer發送消息失敗,也可能導致消費者Consumer消費消息失敗,因此RMQ的消息重試機制就顯得比較重要了,這也是RMQ的一大優勢所在,顯然消息重試機制分2種。

 

 

生產者Producer端重試

生產端發送消息失敗就是指,Producer向MQ發送消息的時候沒有發送成功,導致的原因可能有網絡傳輸失敗等,下面我們就來看看生產端是怎麼處理消息發送失敗的:

  • 配置生產者的重試次數:消息重試發送的次數限制

  • 配置生產者發送消息時的超時等待:在指定時間內如果消息沒有成功發送到MQ就嘗試重新發送

# 如果消息在1秒之內沒有發送成功就重試 重試次數上限爲5
producer.setRetryTimesWhenSendFailed(5);
SendResult sendResult = producer.send(msg,1000);

 

Producer端代碼如下:

public class Producer {
   public static void main(String[] args) throws MQClientException, InterruptedException {
       // 聲明一個生產者,需要一個自定義生產者組(後面我們會介紹這個組的概念和作用)
       DefaultMQProducer producer = new DefaultMQProducer("myTestGroup");
       // 設置集羣的NameServer地址,多個地址之間以分號分隔
       producer.setNamesrvAddr("139.196.184.3:9876;139.196.51.36:9876");
       // 如果消息發送失敗就進行5次重試
       producer.setRetryTimesWhenSendFailed(5);
       // 啓動生產者實例
       producer.start();
       // 模擬發送10條消息 到Topic爲TopicTest,tag爲tagA,消息內容爲Hello RocketMQ +i
       for (int i = 0; i < 4; i++) {
           try {
               Message msg = new Message("TopicTest" ,"TagA",("生產第" + i+"條消息").getBytes(RemotingHelper.DEFAULT_CHARSET)
               );
               // 調用Produce的send方法發送消息
               SendResult sendResult = producer.send(msg,1000);
               System.out.printf("%s%n", sendResult);
           } catch (Exception e) {
               e.printStackTrace();
               Thread.sleep(1000);
           }
       }
       // 發送完消息之後調用producer的shutdown()方法關閉producer
       producer.shutdown();
   }
}

 

 

消費者Consumer端重試

消費者消費消息失敗就是指,Consumer從MQ取到消息進行消費的過程中,由於某些原因導致消費失敗(網絡原因,消息邏輯處理異常,消費者直接宕機等等),下面我們就來看下消費端是怎麼處理消息失敗的:

  • 設置消費最大重試次數:默認是16,當配置的值大於16的時候,第16次之後就會每次重試時間間隔2小時,當配置的值小於等於16時,重試的間隔時間如下圖:

    從Broker的啓動日誌也能發現這一點:

    2018-04-23 19:21:35 INFO main - messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
  • 通過返回重試狀態碼:Consumer提供了2個狀態碼

    • CONSUME_SUCCESS:消息消費成功狀態,如果返回該狀態,那麼對應的這條消息就會從RMQ上被消費完成並移出MQ

    • RECONSUME_LATER:消息消費重試狀態,如果返回該狀態,消費者會在間隔時間內再次嘗試消費該消息,每嘗試一次之後,該消息對應的reconsumeTimes的值+1,默認第一次失敗時爲0,不算重試次數

// 設置最大重試次數,默認是16次
consumer.setMaxReconsumeTimes(5);
// 返回重試狀態
return ConsumeConcurrentlyStatus.RECONSUME_LATER;

 

 

消息消費重試實踐

消費者消費失敗又分多種情況,下面我們將一次次來實踐一遍看看效果:

第一種情況:消費者處理消息邏輯時異常

Consumer端代碼如下:

public class Consumer {
   public static void main(String[] args) throws InterruptedException, MQClientException {
       // 聲明一個消費者consumer,需要傳入一個組
       DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerTest");
       // 設置集羣的NameServer地址,多個地址之間以分號分隔
       consumer.setNamesrvAddr("139.196.184.3:9876;139.196.51.36:9876");
       // 設置consumer的消費策略
       consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
       // 集羣模式消費,廣播消費不會重試
       consumer.setMessageModel(MessageModel.CLUSTERING);
       // 設置最大重試次數,默認是16次
       consumer.setMaxReconsumeTimes(5);
       // 設置consumer所訂閱的Topic和Tag,*代表全部的Tag
       consumer.subscribe("TopicTest", "*");
       // Listener,主要進行消息的邏輯處理,監聽topic,如果有消息就會立即去消費
       consumer.registerMessageListener(new MessageListenerConcurrently() {
           @Override
           public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
               // 獲取第一條消息,進行處理
               try {
                   MessageExt messageExt = msgs.get(0);
                   String msgBody = new String(messageExt.getBody(),"utf-8");
                   System.out.println(" 接收新的消息:消息內容爲:"+msgBody +" == 消息整體爲:"+ msgs);
                   // 模擬消息消費失敗操作
                   if(StringUtils.equals(msgBody,"生產第2條消息")){
                       int a = 1 / 0;
                   }
               } catch (Exception e) {
                   e.printStackTrace();
                   System.out.println(e);
                   // 嘗試重新消費,直接第三次如果還不成功就放棄消費,進行消息消費失敗補償操作
                   if(msgs.get(0).getReconsumeTimes() == 3){
                       // 返回成功狀態碼,消息會在mq上被清掉,但是這是一條失敗的消息,所以我們需要做失敗補償操作
                       // 補償策略:記錄數據庫或者日誌,啓動一個定時腳本去掃描這些失敗的消息,進行相應處理
                       //           或者將失敗的消息放入另一個topic,生產者訂閱該topic,將失敗的消息發送給生產者,生產者重新發送到mq上
                       System.out.println("消息記錄日誌:"+msgs);
                       return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                   }else {
                       // 重試狀態碼,重試機制可配置
                       System.out.println("消息消費失敗,嘗試重試!!!");
                       return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                   }
               }
               return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
           }
       });
       // 調用start()方法啓動consumer
       consumer.start();
       System.out.printf("Consumer1 啓動.%n");
   }
}

在這裏,爲了驗證之前說的Group組的概念,我開了2個Consumer端,屬於同一個組,我們其實可以看到消息是被負載的分給2個Consumer的,我們看下面運行結果:

我們再看下該消息對應的重試次數參數變化:

 

第二種情況:當同一個組中的消費者Consumer宕機之後,MQ會將消息轉發給剩下的其他Consumer處理,包括失敗重試的消息也一樣會轉到其他Consumer中被處理(是不是突然對這個Group組有着莫名的好感)

我們看下面這個例子:我先啓動了2個Consumer,這個重試的消息落在了c1的身上,當重試一次之後,我把c1宕機了,我們來看看c2發生了什麼,第2條消息被轉發到了c2上,而且重試次數也是在之前c1的基礎上操作的;

 

第三種情況:當同一個組下的某個Consumer處理的消息超時的時候,MQ消息就會不斷嘗試處理這條消息,直到發送成功爲止(這個是RMQ內部自己做的重試機制),這種情況是不會轉發給另一個Consumer處理的:生產者生產一條消息,被c2處理了,c2睡眠60秒,在這60秒內,消息都是一直在c2上進行重試(隱式實現),直到我把c2宕機,你會發現,消息纔會被c1處理(上述第二種情況):

MessageExt messageExt = msgs.get(0);
                   String msgBody = new String(messageExt.getBody(), "utf-8");
                   System.out.println(" 接收新的消息:消息內容爲:" + msgBody + " == 消息重試次數:" + messageExt.getReconsumeTimes());
                   Thread.sleep(60000);

 

講到這裏,其實我們的消息重試就差不多講完了,但是有一點一定要注意

注意:消費端的消息重試機制一定要在集羣消費模式下才有效,廣播消費模式下,RMQ是不會進行重試機制的,廣播模式下,消息只消費一次,不管你有沒有成功!!!

 

 

消息重複消費問題

之前我們講過當我們先啓動生產者生產消息,後啓動消費者消費消息時,當多個消費者就有可能消費到同一條消息,就像2個人去領任務,第一個人先領取了任務1,但是還在處理,任務還沒完成,第二個人過來時,也看到了任務1,就也領取了任務1,然後就造成2個人處理了同一個任務,我們可以看下面示例:c1和c2同時處理了第一條消息,很明顯這是不合理的

 

對於上面的問題,我們就需要相應的處理策略,我總結覺得可以從下面2個方面入手

保證消費端處理消息的業務邏輯保持冪等性

如何保證冪等呢,我們主要從以下幾個手段考慮:

  • 冪等性可以自己業務邏輯實現,例如不管邏輯代碼執行多少次,只要是同一個編號處理,得到的結果都是一樣的,例如更新訂單狀態,只要是同一個訂單號,就算重複消費,執行了多個update,最終數據庫還是一樣的結果;

  • 如果不是update這種操作呢,例如insert一條訂單下單成功記錄,那麼此時我們可以通過設置數據庫表某個字段唯一約束,例如訂單號,來解決處理結果的冪等;

  • 如果insert的數據不能設置唯一約束呢,那麼我們還可以啓動一個腳本,定時掃描數據庫表,發現如果是同樣的數據被生成出來,可以刪掉一條,以此來保證重複消費帶來的數據重複;

總之,不管你用什麼辦法,就是假如消息被重複消費了,那麼我們一定要想辦法來保證執行結果的冪等。

 

保證每條消息都有唯一標識,且每條消息只會被處理一次

既然上面是假如消息被重複消費了,那麼當然還有一個辦法就是防止消息被重複消費主要有下面2個手段:

  • 利用一張日誌表來記錄已經處理成功的消息的ID,如果新到的消息ID已經在日誌表中,那麼就不再處理這條消息

  • 我們可以給每條消息自定義一個狀態字段,當生產消息時默認爲未消費狀態,當獲取到消息時,標爲正在消息狀態,當消費完時標爲已消費狀態(這一步可以不做,因爲當一個消息被成功消費完時,其實他也就不在RMQ中了,其他消費者也不會獲取到這條消息)。然後每次消費者消費消息時,都先對消息這個狀態值進行判斷,如果是正在消費或者已消費就不做處理,直接獲取下一條

 

OK,以上就是我們今天所講的消息重試和重複消費問題,希望看完,能對您有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章