消息隊列面試題

1. 項目中為什么使用mq

(1) 解耦

  • 一個模塊, 調(diào)用其他多個模塊的接口, 調(diào)用過程很復(fù)雜, 但又不是必須同步調(diào)用的時候, 就可以將這個調(diào)用最為一條消息放在mq里, 讓被調(diào)用者取訂閱相關(guān)的消息, 此時被調(diào)用的那幾個模塊就可以從mq里得知有一個模塊調(diào)用我了
  • 不用考慮對方調(diào)用是否成功, 超時, 失敗重試等
  • 主要是應(yīng)用了消息隊列的pub-sub模型

(2) 異步

  • 不用mq的同步高延時場景:
    還是以上場景, 模塊A要分別調(diào)用模塊B, 模塊C, 模塊D; 假設(shè)BCD三個模塊分別處理3條sql, 5條sql, 4條sql, 且分別耗時200ms, 250ms, 700ms, 則模塊A要想正常返回, 需要1150ms, 這在用戶體驗上十分不可接受(一般可接受范圍在200ms以內(nèi))
  • 消峰

2. 引入消息隊列后, 有什么壞處

  • 系統(tǒng)可用性降低
  • 系統(tǒng)與MQ之間的交互可能會產(chǎn)生問題
    • 消息重復(fù): 比如系統(tǒng)A本來只應(yīng)該調(diào)用系統(tǒng)B一次, 但是由于系統(tǒng)A和MQ交互發(fā)生問題 ,使得A存了2此請求在mq中, 則B會被請求2次
    • 消息丟失: 少了幾次請求
    • 消息亂序: 調(diào)用次序變了
    • 消費(fèi)者掛掉了: 系統(tǒng)BCD掛掉了, 系統(tǒng)A狂往隊列中加數(shù)據(jù)
  • 一致性問題
    本來系統(tǒng)ABCD都成在能返回給用戶成功, 加入mq后, 調(diào)用變成異步的了, 恰恰BCD中某個模塊失敗了, 但A卻已經(jīng)給用戶正確的返回了

3. 如何保證mq的高可用性

(1) Rabbit mq高可用的做法

  • 普通集群模式
    數(shù)據(jù)只保存在某1個節(jié)點(diǎn)上, 而其它節(jié)點(diǎn)只負(fù)責(zé)響應(yīng)request請求, 然后查找該請求的數(shù)據(jù)在那個節(jié)點(diǎn)上, 把數(shù)據(jù)從其他節(jié)點(diǎn)傳輸?shù)奖竟?jié)點(diǎn)再返回給用戶端
    這種方式?jīng)]有任何高可用性
  • 鏡像集群模式(數(shù)據(jù)副本, n臺機(jī)器保存n分完整數(shù)據(jù), 不存在partition, 只存在replication)
    每個機(jī)器都是完整的queue的數(shù)據(jù), 這幾個節(jié)點(diǎn)的數(shù)據(jù)互為備份, 但是仍然不是分布式的queue, 只能用一臺機(jī)器存儲數(shù)據(jù)
    (通過管理平臺 增加一條鏡像管理策略, 配置所有機(jī)器同部數(shù)據(jù)/某幾臺機(jī)器同步數(shù)據(jù))

(2) kafka高可用性的做法

  • 數(shù)據(jù)分區(qū)(partition)
  • 數(shù)據(jù)備份(每個partition分為leader和follower)

4. mq consumer如何保證消息不被重復(fù)消費(fèi)or如何保證消息的冪等性

(1) kafka
每條消息有一個offset, 可理解為每個消息的一個編號. consumer消費(fèi)后返回offset(基于zookeeper里記錄消費(fèi)到了哪個offset) 或記錄到kafka的一個特定隊列中(__consumer_offsets下面)

如何保證消息重復(fù)消費(fèi)的冪等性
(1) 為什么會出現(xiàn)數(shù)據(jù)的重復(fù)消費(fèi)
比如kafka中, 如果consumer在消費(fèi)后就宕機(jī)了, 而且還沒來得及到zookeeper中記錄自己消費(fèi)的offset
(2) 如何保證冪等性
比如consumer消費(fèi)數(shù)據(jù)時要寫入databse, 可以記錄一個消息主鍵, 重復(fù)消費(fèi)時會報錯但不會重復(fù)保存

5. 如何處理使用mq后消息存在丟失的問題

  • rabbitmq
    (1) 生產(chǎn)者把數(shù)據(jù)發(fā)往mq的時候丟了

    1. rabbitmq可以通過開啟事務(wù)實現(xiàn)避免生產(chǎn)者這端丟數(shù)據(jù). 這種模式是同步機(jī)制, 只等這條消息發(fā)送完畢再發(fā)送下一條消息
      channel.txSelect()
      try{
        // 發(fā)送消息
      }catch (異常){
        channel.txrollback;
        // 再次重試發(fā)送數(shù)據(jù)
      }
      
    2. 或者開啟confirm機(jī)制.
      • 先把channel設(shè)置為confirm機(jī)制,
      • 然后發(fā)送消息,
      • 發(fā)送成功后, rabbitmq接收消息后會回調(diào)生產(chǎn)者本地的回調(diào)接口, 如果mq保存消息成功回調(diào)ack()方法, 如果保存失敗則回調(diào)unack()方法
    這種方法是異步機(jī)制, 發(fā)送完不用等待回調(diào)就可以立刻發(fā)送下一條. 一般選用confirm機(jī)制
    

    (2) mq存儲的數(shù)據(jù)丟了

    • rabbitmq開啟持久化
      • a. 先把queue的元數(shù)據(jù)開啟持久化
      • b. 把diliverymode設(shè)置為2, 讓queue里的數(shù)據(jù)持久化
        但此時仍存在一個風(fēng)險就是, 數(shù)據(jù)存到內(nèi)存后, 還沒來得及存到磁盤, mq就掛了, 導(dǎo)致數(shù)據(jù)丟失

    (3) 消費(fèi)者處理失敗導(dǎo)致消息丟失
    消費(fèi)者從mq收到消息后就掛掉了, 但還沒來得及消費(fèi), 而mq卻認(rèn)為消費(fèi)者取走就代表消費(fèi)完畢
    這種一般是開啟了消費(fèi)者的auto ack機(jī)制, 這樣會讓rabbitmq認(rèn)為收到消息就是處理完畢, 此時若在處理過程中consumer宕機(jī), 就會出現(xiàn)數(shù)據(jù)丟失問題
    所以應(yīng)該關(guān)閉autoack機(jī)制, 在consumer處理完消息后在手動進(jìn)行ack

  • kafka
    (1) kafka存儲的數(shù)據(jù)丟失
    因為kafka的partition通過leader和follower進(jìn)行數(shù)據(jù)高可用, 有時數(shù)據(jù)只寫到了leader的內(nèi)存, 還沒復(fù)制到follower時, leader就宕機(jī)了, 此后將follower選舉成leader的時候數(shù)據(jù)就丟失了
    解決辦法: 設(shè)置4個參數(shù)

    1. kafka服務(wù)期設(shè)置
      • 為topic設(shè)置replication.factors大于1,
        確保數(shù)據(jù)至少有2個副本
      • min.insync.replicats:
        kafka的leader會對follower進(jìn)行心跳, 如果超時認(rèn)為該follower宕機(jī). 該參數(shù)必須大于等于2, 表示數(shù)據(jù)至少有1個follower可以和leader正常通信
    2. producer段設(shè)置
      • ack=all:
        每條數(shù)據(jù)必須寫入所有的follower才認(rèn)為是寫入成功了
      • retries=MAX:
        一旦失敗, 就自動無限重試的重發(fā)數(shù)據(jù). 該參數(shù)設(shè)置后配合ack=all, 使得當(dāng)leader未寫成功所有follower就宕機(jī)后leader切換, producer可以再次把數(shù)據(jù)發(fā)送給新leader

    (2) kafka消費(fèi)端丟數(shù)據(jù)
    原理一樣. kafka的自動提交offset機(jī)制, 導(dǎo)致consumer收到消息成功但處理過程中失敗時造成數(shù)據(jù)丟失. 解決辦法也是關(guān)閉自動提交offset, 來手動提交offset

6. 如何保證消息的順序性

(1) 什么時候rabbitMq的數(shù)據(jù)會出現(xiàn)順序性問題
當(dāng)producer只有1個, consumer有多個, 且mq以隊列模式存在(每個consumer依次從mq中拿數(shù)據(jù)), 這種時候, 即使mq中的消息不亂序, 但由于consumer處理消息的速度不同, 最終產(chǎn)生結(jié)果的順序會不同(比如consumer是插入數(shù)據(jù)到數(shù)據(jù)庫的話)
解決辦法: 需要保證順序的消息都放到1個queue里, 同時只被1個consumer消費(fèi)

(2) 什么時候kafka出現(xiàn)順序問題

  1. kafka可以保證寫入一個partition的數(shù)據(jù)時有序的, 所以可以在寫入數(shù)據(jù)時指定一個key
producer.send(new ProducerRecord<>(topic,partition,key, msg));

比如: 設(shè)置訂單id為key, 這個key相關(guān)的數(shù)據(jù)全都會發(fā)送到1個partition中

  1. kafka的另一個原則:
    1個partition只能被1個消費(fèi)者消費(fèi). 即如果partition有3個, 而開啟了4個consumer, 那有1個consumer會閑置
  2. 但是, 如果1個consumer內(nèi)開啟了多個線程處理數(shù)據(jù), 則還會出現(xiàn)數(shù)據(jù)順序的問題.
    解決辦法:
    如果確實1個consumer需要多個線程并發(fā)加速處理數(shù)據(jù), 那么可以為每個線程開啟一個內(nèi)存queue, 從kafka中得到數(shù)據(jù)時, 把key按照hash分發(fā)到不同的queue. 例如: 1個訂單id的消息, 既可以保證生產(chǎn)者生產(chǎn)的數(shù)據(jù)在partition中有序, 也可以保證消費(fèi)者在多線程下消費(fèi)的順序性

7. 有幾百萬消息積壓在kafka消息隊列怎么辦

????消息積壓是因為consumer出問題或下線了, 重啟恢復(fù)consumer后, 首先創(chuàng)建1個新的topic, 讓這個新的topic擁有的partition數(shù)量是原來的10倍, 修改原來的consumer代碼邏輯為: 把老topic的數(shù)據(jù)消費(fèi)后就存入新的topic. 然后再啟動10倍的consumer消費(fèi)這個新topic下的消息.
為什么不能直接啟動10倍的consumer消費(fèi)老topic?
????因為kafka里1個partition只能被1個consumer消費(fèi), 老topic的partition數(shù)量不變的話, 即使新增consumer, 那些consumer也會被閑置

8. 如果讓你來開發(fā)一個mq中間件, 如何設(shè)計這個架構(gòu)

面試官心理分析:
????其實聊到這個問題,一般面試官要考察兩塊:
你有沒有對某一個消息隊列做過較為深入的原理的了解,或者從整體了解把握住一個消息隊列的架構(gòu)原理。
看看你的設(shè)計能力,給你一個常見的系統(tǒng),就是消息隊列系統(tǒng),看看你能不能從全局把握一下整體架構(gòu)設(shè)計,給出一些關(guān)鍵點(diǎn)出來。
說實話,問類似問題的時候,大部分人基本都會蒙,因為平時從來沒有思考過類似的問題,大多數(shù)人就是平時埋頭用,從來不去思考背后的一些東西。類似的問題,比如,如果讓你來設(shè)計一個 Spring 框架你會怎么做?如果讓你來設(shè)計一個 Dubbo 框架你會怎么做?如果讓你來設(shè)計一個 MyBatis 框架你會怎么做?
回答思路:
???? 首先, 讓mq支持?jǐn)?shù)據(jù)的分布式存儲, 其次支持高可用(數(shù)據(jù)副本), 支持?jǐn)?shù)據(jù)的零丟失(數(shù)據(jù)副本的一致性問題)

rabbitMQ基本概念

  • 概念:
    • Brocker:消息隊列服務(wù)器實體。
    • Exchange:消息交換機(jī),指定消息按什么規(guī)則,路由到哪個隊列。
    • Queue:消息隊列,每個消息都會被投入到一個或者多個隊列里。
    • Binding:綁定,它的作用是把exchange和queue按照路由規(guī)則binding起來。
    • Routing Key:路由關(guān)鍵字,exchange根據(jù)這個關(guān)鍵字進(jìn)行消息投遞。
    • Vhost:虛擬主機(jī),一個broker里可以開設(shè)多個vhost,用作不同用戶的權(quán)限分離。
    • Producer:消息生產(chǎn)者,就是投遞消息的程序。
    • Consumer:消息消費(fèi)者,就是接受消息的程序。
    • Channel:消息通道,在客戶端的每個連接里,可建立多個channel,每個channel代表一個會話任務(wù)。
  • 消息隊列的使用過程大概如下:
    • 消息接收
      • 客戶端連接到消息隊列服務(wù)器,打開一個channel。
      • 客戶端聲明一個exchange,并設(shè)置相關(guān)屬性。
      • 客戶端聲明一個queue,并設(shè)置相關(guān)屬性。
      • 客戶端使用routing key,在exchange和queue之間建立好綁定關(guān)系。
    • 消息發(fā)布
      • 客戶端投遞消息到exchange。
      • xchange接收到消息后,就根據(jù)消息的key和已經(jīng)設(shè)置的binding,進(jìn)行消息路由,將消息投遞到一個或多個隊列里。
  • AMQP 里主要要說兩個組件:
    Exchange 和 Queue
    綠色的 X 就是 Exchange ,紅色的是 Queue ,這兩者都在 Server 端,又稱作 Broker
    這部分是 RabbitMQ 實現(xiàn)的,而藍(lán)色的則是客戶端,通常有 Producer 和 Consumer 兩種類型。
  • Exchange通常分為四種:
    • fanout:該類型路由規(guī)則非常簡單,會把所有發(fā)送到該Exchange的消息路由到所有與它綁定的Queue中,相當(dāng)于廣播功能
    • direct:該類型路由規(guī)則會將消息路由到binding key與routing key完全匹配的Queue中
    • topic:與direct類型相似,只是規(guī)則沒有那么嚴(yán)格,可以模糊匹配和多條件匹配
    • headers:該類型不依賴于routing key與binding key的匹配規(guī)則來路由消息,而是根據(jù)發(fā)送的消息內(nèi)容中的headers屬性進(jìn)行匹配

kafka基本概念:

topic: 隊列, 相當(dāng)于rabbitmq的queue
partition: 數(shù)據(jù)的分片(分布式存儲數(shù)據(jù)), rabbitmq中沒有分布式存儲的概念

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容