文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :
免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
滴滴面試:Rocketmq消息0丟失,如何實現?
尼恩說在前面
在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團、螞蟻、得物的面試資格,遇到很多很重要的面試題:
Rocketmq消息0丟失,如何實現?
Rocketmq如何保證消息可靠?
最近有小夥伴在面試滴滴,都到了相關的面試題,可以說是逢面必問。
小夥伴沒有系統的去梳理和總結,所以支支吾吾的說了幾句,面試官不滿意,面試掛了。
所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。
當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V175版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。
《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到文末公號【技術自由圈】獲取
消息的發送流程
Rocketmq和KafKa類似(實質上,最早的Rocketmq 就是KafKa 的Java版本),一條消息從生產到被消費,將會經歷三個階段:
- 生產階段,Producer 新建消息,而後經過網絡將消息投遞給 MQ Broker。這個發送可能會發生丟失,比如網絡延遲不可達等。
- 存儲階段,消息將會存儲在 Broker 端磁盤中,Broker 根據刷盤策略持久化到硬盤中,剛收到Producer的消息在內存中了,但是如果Broker 異常宕機了,導致消息丟失。
- 消費階段, Consumer 將會從 Broker 拉取消息
以上任一階段, 都可能會丟失消息,只要這三個階段0丟失,就能夠完全解決消息丟失的問題。
宏觀層面的大的階段和流程,Rocketmq和KafKa類似的。
KafKa 零丟失,具體的文章:
生產階段如何實現0丟失方式
生產階段有三種send方法:
- 同步發送
- 異步發送
- 單向發送。
三種send方法的 客戶端api,具體如下:
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer}
*/
// 同步發送
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {}
// 異步發送,sendCallback作爲回調
public void send(Message msg,SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {}
// 單向發送,不關心發送結果,最不靠譜
public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {}
produce要想發消息時保證消息不丟失,可以採用同步發送的方式去發消息,send消息方法只要不拋出異常,就代表發送成功。
發送成功會有多個SendResult 狀態,以下對每個狀態進行說明:
- SEND_OK:消息發送成功,Broker刷盤、主從同步成功
- FLUSH_DISK_TIMEOUT:消息發送成功,但是服務器同步刷盤(默認爲異步刷盤)超時(默認超時時間5秒)
- FlUSH_SLAVE_TIMEOUT:消息發送成功,但是服務器同步複製(默認爲異步複製)到Slave時超時(默認超時時間5秒)
- SLAVE_NOT_AVAILABLE:Broker從節點不存在
注意:同步發送只要返回以上四種狀態,就代表該消息在生產階段消息正確的投遞到了RocketMq,生產階段沒有丟失。
如果業務要求嚴格,可以只取SEND_OK標識消息發送成功, 其他類型的數據,採用40歲老架構師尼恩給大家設計的,業務維度的 終極0丟失保護措施:本地消息表+定時掃描 (具體參見本文末尾)
是同步發送還是異步發送
根據尼恩的架構設計40個黃金法則 ,AP 和 CP 是天然的矛盾, 到底是 CP 還是 AP的 需要權衡,
-
同步發送的方式 是 CP ,高可靠,但是性能低。
-
異步發送的方式 是 AP ,低可靠,但是性能高。
爲了高可靠(CP),可以採取同步發送的方式進行發送消息,發消息的時候會同步阻塞等待broker返回的結果,如果沒成功,則不會收到SendResult,這種是最可靠的。
其次是異步發送,再回調方法裏可以得知是否發送成功。
最後,單向發送(OneWay)是最不靠譜的一種發送方式,我們無法保證消息真正可達。
當然,具體的如何選擇高可用方案,還是要看業務。 也可以選擇異步發送 + 業務維度的 終極0丟失保護措施 , 實現消息的0丟失。
生產端的失敗重試策略
發送消息如果失敗或者超時了,則會自動重試。
同步發送默認是重試三次,可以根據api進行更改,比如改爲10次:
producer.setRetryTimesWhenSendFailed(10);
其他模式是重試1次,具體請參見源碼
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer#sendDefaultImpl(Message, CommunicationMode, SendCallback, long)}
*/
// 自動重試次數,this.defaultMQProducer.getRetryTimesWhenSendFailed()默認爲2,如果是同步發送,默認重試3次,否則重試1次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
// 選擇發送的消息queue
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
try {
// 真正的發送邏輯,sendKernelImpl。
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
// 如果發送失敗了,則continue,意味着還會再次進入for,繼續重試發送
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
// 發送成功的話,將發送結果返回給調用者
return sendResult;
default:
break;
}
} catch (RemotingException e) {
continue;
} catch (...) {
continue;
}
}
}
上面的核心邏輯中,調用sendKernelImpl真正的去發送消息
通過核心的發送邏輯,可以看出如下:
-
同步發送場景的重試次數是1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() =3,其他方式默認1次。
-
this.defaultMQProducer.getRetryTimesWhenSendFailed()默認是2,我們可以手動設置
producer.setRetryTimesWhenSendFailed(10);
-
如果是同步發送sync,且發送失敗了,則continue,意味着還會再次進入for,繼續重試發送
同步模式下,可以設置嚴格的消息重試機制,比如設置 RetryTimes爲一個較大的值如10。當出現網絡的瞬時抖動時,消息發送可能會失敗,retries 較大,能夠自動重試消息發送,避免消息丟失。
Broker端保證消息不丟失的方法:
首先,尼恩想說正常情況下,只要 Broker 在正常運行,就不會出現丟失消息的問題。
但是如果 Broker 出現了故障,比如進程死掉了或者服務器宕機了,還是可能會丟失消息的。
如果確保萬無一失,實現Broker端保證消息不丟失,有兩板斧:
- Broker端第一板斧:設置嚴格的副本同步機制
- Broker端第二板斧:設置嚴格的消息刷盤機制
Broker端第一板斧:設置嚴格的副本同步機制
RocketMQ 通過多副本機制來解決的高可用,核心思想也挺簡單的:如果數據保存在一臺機器上你覺得可靠性不夠,那麼我就把相同的數據保存到多臺機器上,某臺機器宕機了可以由其它機器提供相同的服務和數據。
首先,Broker需要集羣部署,通過主從模式包括 topic 數據的高可用。
爲了消息0丟失,可以配置設置嚴格的副本同步機制,等Master 把消息同步給 Slave後,纔去通知Producer說消息ok。
設置嚴格的副本同步機制 , RocketMQ 修改broker刷盤配置如下:
所以我們還可以配置不僅是等Master刷完盤就通知Producer,而是等Master和Slave都刷完盤後纔去通知Producer說消息ok了。
## 默認爲 ASYNC_MASTER
brokerRole=SYNC_MASTER
Broker端第二板斧:設置嚴格的消息刷盤機制
RocketMQ持久化消息分爲兩種:同步刷盤和異步刷盤。
RocketMQ和kafka一樣的,刷盤的方式有同步刷盤和異步刷盤兩種。
-
同步刷盤指的是:生產者消息發過來時,只有持久化到磁盤,RocketMQ、kafka的存儲端Broker才返回一個成功的ACK響應,這就是同步刷盤。它保證消息不丟失,但是影響了性能。
-
異步刷盤指的是:消息寫入PageCache緩存,就返回一個成功的ACK響應,不管消息有沒有落盤,就返回一個成功的ACK響應。這樣提高了MQ的性能,但是如果這時候機器斷電了,就會丟失消息。
同步刷盤和異步刷盤的區別如下:
- 同步刷盤:當數據寫如到內存中之後立刻刷盤(同步),在保證刷盤成功的前提下響應client。
- 異步刷盤:數據寫入內存後,直接響應client。異步將內存中的數據持久化到磁盤上。
同步刷盤和異步輸盤的優劣:
- 同步刷盤保證了數據的可靠性,保證數據不會丟失。
- 同步刷盤效率較低,因爲client獲取響應需要等待刷盤時間,爲了提升效率,通常採用批量輸盤的方式,每次刷盤將會flush內存中的所有數據。(若底層的存儲爲mmap,則每次刷盤將刷新所有的dirty頁)
- 異步刷盤不能保證數據的可靠性.
- 異步刷盤可以提高系統的吞吐量.
- 常見的異步刷盤方式有兩種,分別是定時刷盤和觸發式刷盤。定時刷盤可設置爲如每1s刷新一次內存.觸發刷盤爲當內存中數據到達一定的值,會觸發異步刷盤程序進行刷盤。
Broker端第二板斧:設置嚴格的消息刷盤機制,設置爲Kafka同步刷盤。
RocketMQ默認情況是異步刷盤,Broker收到消息後會先存到cache裏,然後通知Producer說消息我收到且存儲成功。 Broker起個線程異步的去持久化到磁盤中,但是Broker還沒持久化到磁盤就宕機的話,消息就丟失了。
同步刷盤的話是收到消息存到cache後並不會通知Producer說消息已經ok了,而是會等到持久化到磁盤中後纔會通知Producer說消息完事了。
-
同步刷盤的方式 是 CP ,高可靠,但是性能低。
-
異步刷盤的方式 是 AP ,低可靠,但是性能高。
根據尼恩的架構設計40個黃金法則 ,AP 和 CP 是天然的矛盾, 到底是 CP 還是 AP的 需要權衡,
爲了高可靠(CP),可以採取同步刷盤保障了消息不會丟失,但是性能不如異步高。
如何設置RocketMQ同步刷盤?
RocketMQ 修改broker刷盤配置如下:
## 默認情況爲 ASYNC_FLUSH,修改爲同步刷盤:SYNC_FLUSH,實際場景看業務,同步刷盤效率肯定不如異步刷盤高。
flushDiskType = SYNC_FLUSH
對應的RocketMQ源碼類如下:
package org.apache.rocketmq.store.config;
public enum FlushDiskType {
// 同步刷盤
SYNC_FLUSH,
// 異步刷盤(默認)
ASYNC_FLUSH
}
異步刷盤默認10s執行一次,源碼如下:
/*
* {@link org.apache.rocketmq.store.CommitLog#run()}
*/
while (!this.isStopped()) {
try {
// 等待10s
this.waitForRunning(10);
// 刷盤
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
Broker端0丟失的配置總結
Broker端的配置,若想很嚴格的保證Broker存儲消息階段消息不丟失,則需要如下配置
# master 節點配置
flushDiskType = SYNC_FLUSH
brokerRole=SYNC_MASTER
# slave 節點配置
brokerRole=slave
flushDiskType = SYNC_FLUSH
上面這個配置含義是:
Producer發消息到Broker後,Broker的Master節點先持久化到磁盤中,然後同步數據給Slave節點,Slave節點同步完且落盤完成後纔會返回給Producer說消息ok了。
嚴格的消息刷盤機制 + 嚴格的消息同步機制,能夠確保 Broker端保證消息不丟失
當然,根據尼恩的架構設計40個黃金法則 ,AP 和 CP 是天然的矛盾, 到底是 CP 還是 AP的 需要權衡,
Consumer(消費者)保證消息不丟失的方法:
如果要保證 Consumer(消費者)0 丟失, Consumer 端的策略是啥呢?
普通的情況下,rocketMq拉取消息後,執行業務邏輯。
一旦Consumer執行成功,將會返回一個ACK響應給 Broker,這時MQ就會修改offset,將該消息標記爲已消費,不再往其他消費者推送消息。
如果出現消費超時(默認15分鐘)、拉取消息後消費者服務宕機等消費失敗的情況,此時的Broker由於沒有等到消費者返回的ACK,會向同一個消費者組中的其他消費者間隔性的重發消息,直到消息返回成功(默認是重複發送16次,若16次還是沒有消費成功,那麼該消息會轉移到死信隊列,人工處理或是單獨寫服務處理這些死信消息)
但是 消費者,也有兩種消費模式:
- 同步消費
- 異步消息
rocketMq 在併發消費模式下,默認有20個消費線程:
關於Rocketmq 的消息消費核心架構,請參見尼恩的硬核文章:
如何保證客戶端的高可用,兩種場景:
- 同步消費場景手動發送CONSUME_SUCCESS ,保證 消息者的0丟失
- 異步消費場景,需要通過業務維度的 終極0丟失保護措施:本地消息表+定時掃描 ,保證 消息者的0丟失
1、同步消費發送CONSUME_SUCCESS
同步消費指的是拉取消息的線程,先把消息拉取到本地,然後進行業務邏輯,業務邏輯完成後手動進行ack確認,這時候纔會真正的代表消費完成。舉個例子
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : msgs) {
String str = new String(msg.getBody());
// 消費者 線程 同步進行 業務處理
System.out.println(str);
}
// ack,只有等上面一系列邏輯都處理完後,
// 發 CONSUME_SUCCESS纔會通知broker說消息消費完成,
// 如果上面發生異常沒有走到這步ack,則消息還是未消費狀態。
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
2、異步消費場景,如何保證0丟失
rocketMq 在併發消費模式下,默認有20個消費線程,但是這個還是有限制的。
如果實現高性能呢?
-
可以一方面去進行線程擴容, 比如通過修改配置,擴容到100個rocketMq 同步消費線程,但是這個會在沒有 活兒乾的場景,浪費寶貴的CPU資源。
-
可以另一方便通過異步的 可動態擴容的業務專用線程池,去完成 消費的業務處理。那麼,如果是設置了業務的專用線程池,則需要通過業務維度的 終極0丟失保護措施:本地消息表+定時掃描 ,保證 消息者的0丟失
關於可動態擴容的業務專用線程池,請參考尼恩的文章:
業務維度的 終極0丟失保護措施:本地消息表+定時掃描
40歲老架構師尼恩慎重提示:前面三板斧,並不能保證100%的0丟失。
因爲百密一疏,任何環節的異常,都有可能導致數據丟失。
有沒有業務維度的 終極保護措施呢? 有:本地消息表+定時掃描
本地消息表+定時掃描 方案,和本地消息表事務機制類似,也是採用 本地消息表+定時掃描 相結合的架構方案。
大概流程如下圖
1、設計一個本地消息表,可以存儲在DB裏,或者其它存儲引擎裏,用戶保存消息的消費狀態
2、Producer 發送消息之前,首先保證消息的發生狀態,並且初始化爲待發送;
3、如果消費者(如庫存服務)完成的消費,則通過RPC,調用Producer 去更新一下消息狀態;
4、Producer 利用定時任務掃描 過期的消息(比如10分鐘到期),再次進行發送。
在這裏尼恩想說的是: 本地消息表+定時掃描 的架構方案 ,是業務層通過額外的機制來保證消息數據發送的完整性,是一種很重的方案。 這個方案的兩個特點:
- CP 不是 AP,性能低
- 需要 做好冪等性設計
如果降低業務維度的 終極0丟失保護措施帶來的性能耗損?
可以減少本地消息表的規模,對於正常投遞的消息不做跟蹤,只把生產端發送失敗的消息、消費端消費失敗的消息記錄到數據庫,並啓動一個定時任務,掃描發送失敗的消息,重新發送直到超過閾值(如10次),超過之後,發送郵件或短信通知人工介入處理。
CP 不是 AP的 需要權衡,請參見全網最好的架構設計個黃金法則,尼恩的 專門文章具體如下:
全網最好的冪等性 方案,請參見尼恩的 專門文章, 具體如下:
RocketMQ的0丟失的最佳實踐
-
Producer端:使用同步發送方式,發送消息。
記住,一定要使用帶有回調通知的 send 方法。
-
Producer端:同步模式下,可以設置嚴格的消息重試機制,比如設置 RetryTimes爲一個較大的值如10。當出現網絡的瞬時抖動時,消息發送可能會失敗,retries 較大,能夠自動重試消息發送,避免消息丟失。。
-
Broker 端設置嚴格的副本同步機制 。
-
Broker 端 設置嚴格的消息刷盤機制。
-
Consumer 端 確保消息消費完成再提交。可以使用同步消費,併發送CONSUME_SUCCESS。
-
業務維度的的0丟失架構, 採用 本地消息表+定時掃描 架構方案,實現業務維度的 0丟失,100%可靠性。
如上,就是尼恩爲大家梳理的,史上最牛掰的 答案, 全網最爲爆表的方案。按照尼恩的套取去回到, 面試官一定驚到掉下巴。 offer直接奉上。此答案大家可以收藏一起,有時間看看。
說在最後:有問題找老架構取經
Rocketmq消息0丟失,如何實現?,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。
最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。
在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走。
在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。
另外,如果沒有面試機會,可以找尼恩來改簡歷、做幫扶。
遇到職業難題,找老架構取經, 可以省去太多的折騰,省去太多的彎路。
尼恩指導了大量的小夥伴上岸,前段時間,剛指導一個40歲+被裁小夥伴,拿到了一個年薪100W的offer。
狠狠卷,實現 “offer自由” 很容易的, 前段時間一個武漢的跟着尼恩捲了2年的小夥伴, 在極度嚴寒/痛苦被裁的環境下, offer拿到手軟, 實現真正的 “offer自由” 。
技術自由的實現路徑:
實現你的 架構自由:
《阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了》
《峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?》
… 更多架構文章,正在添加中
實現你的 響應式 自由:
這是老版本 《Flux、Mono、Reactor 實戰(史上最全)》
實現你的 spring cloud 自由:
《Spring cloud Alibaba 學習聖經》 PDF
《分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)》
實現你的 linux 自由:
實現你的 網絡 自由:
《網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!》
實現你的 分佈式鎖 自由:
實現你的 王者組件 自由:
《隊列之王: Disruptor 原理、架構、源碼 一文穿透》
《緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)》
《Java Agent 探針、字節碼增強 ByteBuddy(史上最全)》