RocketMQ重試機制
訊息重試分為兩種:Producer傳送訊息的重試 和 Consumer訊息消費的重試。
一、Producer端重試
Producer端重試是指: Producer往MQ上發訊息沒有傳送成功,比如網路原因導致生產者傳送訊息到MQ失敗。
看一下程式碼:
@Slf4j
public class RocketMQTest {
/**
* 生產者組
*/
private static String PRODUCE_RGROUP = "test_producer";
public static void main(String[] args) throws Exception {
//1、建立生產者物件
DefaultMQProducer producer = new DefaultMQProducer(PRODUCE_RGROUP);
//設定重試次數(預設2次)
producer.setRetryTimesWhenSendFailed(3000);
//繫結name server
producer.setNamesrvAddr("74.49.203.55:9876");
producer.start();
//建立訊息
Message message = new Message("topic_family", ("小小今年3歲" ).getBytes());
//傳送 這裡填寫超時時間是5毫秒 所以每次都會傳送失敗
SendResult sendResult = producer.send(message,5);
log.info("輸出生產者資訊={}",sendResult);
}
}
超時重試
針對網上說的超時異常會重試的說法都是錯誤的,想想都覺得可怕,我查的所以文章都說超時異常都會重試,難道這麼多人都沒有去測試一下 或者去看個原始碼。
我發現這個問題,是因為我上面超時時間設定為5毫秒 ,按照正常肯定會報超時異常,但我設定1次重試和3000次的重試,雖然最終都會報下面異常,但輸出錯誤時間報
顯然不應該是一個級別。但測試發現無論我設定的多少次的重試次數,報異常的時間都差不多。
org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout
針對這個疑惑,我去看了原始碼之後,才恍然大悟。
/**
* 說明 抽取部分程式碼
*/
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) {
//1、獲取當前時間
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev ;
//2、去伺服器看下有沒有主題訊息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
//3、通過這裡可以很明顯看出 如果不是同步傳送訊息 那麼訊息重試只有1次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
//4、根據設定的重試次數,迴圈再去獲取伺服器主題訊息
for (times = 0; times < timesTotal; times++) {
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
beginTimestampPrev = System.currentTimeMillis();
long costTime = beginTimestampPrev - beginTimestampFirst;
//5、前後時間對比 如果前後時間差 大於 設定的等待時間 那麼直接跳出for迴圈了 這就說明連線超時是不進行多次連線重試的
if (timeout < costTime) {
callTimeout = true;
break;
}
//6、如果超時直接報錯
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
}
}
通過這段原始碼很明顯可以看出以下幾點
- 如果是
非同步傳送
那麼重試次數只有1次 - 對於同步而言,
超時異常也是不會再去重試
。 - 如果發生重試是在一個for 迴圈裡去重試,所以它是立即重試而不是隔一段時間去重試。
真是實踐出真知!!!
二、 Consumer端重試
消費端比較有意思,而且在實際開發過程中,我們也更應該考慮的是消費端的重試。
消費者端的失敗主要分為2種情況,Exception
和 Timeout
。
1、Exception
@Slf4j
@Component
public class Consumer {
/**
* 消費者實體物件
*/
private DefaultMQPushConsumer consumer;
/**
* 消費者組
*/
public static final String CONSUMER_GROUP = "test_consumer";
/**
* 通過建構函式 例項化物件
*/
public Consumer() throws MQClientException {
consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr("47.99.203.55:9876;47.99.203.55:9877");
//訂閱topic和 tags( * 代表所有標籤)下資訊
consumer.subscribe("topic_family", "*");
//註冊消費的監聽 並在此監聽中消費資訊,並返回消費的狀態資訊
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
//1、獲取訊息
Message msg = msgs.get(0);
try {
//2、消費者獲取訊息
String body = new String(msg.getBody(), "utf-8");
//3、獲取重試次數
int count = ((MessageExt) msg).getReconsumeTimes();
log.info("當前消費重試次數為 = {}", count);
//4、這裡設定重試大於3次 那麼通過儲存資料庫 人工來兜底
if (count >= 2) {
log.info("該訊息已經重試3次,儲存資料庫。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//直接丟擲異常
throw new Exception("=======這裡出錯了============");
//return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
//啟動監聽
consumer.start();
}
}
這裡的程式碼意思很明顯: 主動丟擲一個異常,然後如果超過3次,那麼就不繼續重試下去,而是將該條記錄儲存到資料庫由人工來兜底。
看下執行結果
注意
消費者和生產者的重試還是有區別的,主要有兩點
1、預設重試次數:Product預設是2次,而Consumer預設是16次。
2、重試時間間隔:Product是立刻重試,而Consumer是有一定時間間隔的。它照1S,5S,10S,30S,1M,2M····2H
進行重試。
2、Timeout
說明
這裡的超時異常並非真正意義上的超時,它指的是指獲取訊息後,因為某種原因沒有給RocketMQ返回消費的狀態,即沒有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS
或 return ConsumeConcurrentlyStatus.RECONSUME_LATER
。
那麼 RocketMQ會認為該訊息沒有傳送,會一直髮送。因為它會認為該訊息根本就沒有傳送給消費者,所以肯定沒消費。
做這個測試很簡單。
//1、消費者獲得訊息
String body = new String(msg.getBody(), "utf-8");
//2、獲取重試次數
int count = ((MessageExt) msg).getReconsumeTimes();
log.info("當前消費重試次數為 = {}", count);
//3、這裡睡眠60秒
Thread.sleep(60000);
log.info("休眠60秒 看還能不能走到這裡。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
當獲得 當前消費重試次數為 = 0 後 , 關掉該程式。再重新啟動該程式,那麼依然能夠獲取該條訊息
consumer消費者 當前消費重試次數為 = 0
休眠60秒 看還能不能走到這裡。topic=topic_family,keys=1a2b3c4d5f,msg=小小今年3歲
只要自己變優秀了,其他的事情才會跟著好起來(上將2)