滴滴面試:Rocketmq消息0丟失,如何實現?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩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丟失,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的 需要權衡,

一張圖總結架構設計的40個黃金法則

  • 同步發送的方式 是 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的 需要權衡,

一張圖總結架構設計的40個黃金法則

爲了高可靠(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的 需要權衡,

一張圖總結架構設計的40個黃金法則

Consumer(消費者)保證消息不丟失的方法:

如果要保證 Consumer(消費者)0 丟失, Consumer 端的策略是啥呢?

普通的情況下,rocketMq拉取消息後,執行業務邏輯。

一旦Consumer執行成功,將會返回一個ACK響應給 Broker,這時MQ就會修改offset,將該消息標記爲已消費,不再往其他消費者推送消息。

如果出現消費超時(默認15分鐘)、拉取消息後消費者服務宕機等消費失敗的情況,此時的Broker由於沒有等到消費者返回的ACK,會向同一個消費者組中的其他消費者間隔性的重發消息,直到消息返回成功(默認是重複發送16次,若16次還是沒有消費成功,那麼該消息會轉移到死信隊列,人工處理或是單獨寫服務處理這些死信消息)

但是 消費者,也有兩種消費模式:

  • 同步消費
  • 異步消息

rocketMq 在併發消費模式下,默認有20個消費線程:

在這裏插入圖片描述

關於Rocketmq 的消息消費核心架構,請參見尼恩的硬核文章:

阿里面試:說說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的 需要權衡,請參見全網最好的架構設計個黃金法則,尼恩的 專門文章具體如下:

一張圖總結架構設計的40個黃金法則

全網最好的冪等性 方案,請參見尼恩的 專門文章, 具體如下:

最系統的冪等性方案:一鎖二判三更新

在這裏插入圖片描述

RocketMQ的0丟失的最佳實踐

  1. Producer端:使用同步發送方式,發送消息。

    記住,一定要使用帶有回調通知的 send 方法。

  2. Producer端:同步模式下,可以設置嚴格的消息重試機制,比如設置 RetryTimes爲一個較大的值如10。當出現網絡的瞬時抖動時,消息發送可能會失敗,retries 較大,能夠自動重試消息發送,避免消息丟失。。

  3. Broker 端設置嚴格的副本同步機制 。

  4. Broker 端 設置嚴格的消息刷盤機制。

  5. Consumer 端 確保消息消費完成再提交。可以使用同步消費,併發送CONSUME_SUCCESS。

  6. 業務維度的的0丟失架構, 採用 本地消息表+定時掃描 架構方案,實現業務維度的 0丟失,100%可靠性。

如上,就是尼恩爲大家梳理的,史上最牛掰的 答案, 全網最爲爆表的方案。按照尼恩的套取去回到, 面試官一定驚到掉下巴。 offer直接奉上。此答案大家可以收藏一起,有時間看看。

在這裏插入圖片描述

說在最後:有問題找老架構取經

Rocketmq消息0丟失,如何實現?,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走。

在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來改簡歷、做幫扶。

遇到職業難題,找老架構取經, 可以省去太多的折騰,省去太多的彎路。

尼恩指導了大量的小夥伴上岸,前段時間,剛指導一個40歲+被裁小夥伴,拿到了一個年薪100W的offer。

狠狠卷,實現 “offer自由” 很容易的, 前段時間一個武漢的跟着尼恩捲了2年的小夥伴, 在極度嚴寒/痛苦被裁的環境下, offer拿到手軟, 實現真正的 “offer自由” 。

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

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