前言
上節我們介紹了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,以上就是我們今天所講的消息重試和重複消費問題,希望看完,能對您有所幫助。