Java面試—消息隊(duì)列

消息隊(duì)列面試題

題目來自于中華石杉,解決方案根據(jù)自己的思路來總結(jié)而得。
題目主要如下:

1. 為什么要引入消息隊(duì)列?

消息隊(duì)列的引入可以解決3個(gè)核心問題:

  • 解耦
  • 異步
  • 削峰
  1. 解耦
    在一個(gè)項(xiàng)目中,如果一個(gè)模塊A產(chǎn)生的一個(gè)關(guān)鍵數(shù)據(jù),需要調(diào)用其他模塊接口服務(wù);而需要調(diào)用的接口很多,又不確定之后是否還需要將數(shù)據(jù)傳給其他模塊的接口時(shí)。這時(shí)可以使用消息隊(duì)列,使用了消息隊(duì)列之后,模塊A不需要在對(duì)接各個(gè)模塊,而是直接對(duì)接消息隊(duì)列。這樣一來。當(dāng)其他的模塊需要這個(gè)數(shù)據(jù)時(shí),也不用再修改A數(shù)據(jù),而是去到MQ中訂閱這個(gè)Topic。使得模塊A與其他模塊之間耦合度降低。


    解耦
  2. 異步
    在一個(gè)項(xiàng)目中,如果模塊A的請(qǐng)求處理需要20ms,而模塊A又依賴了模塊B,模塊C ,模塊D 。在A請(qǐng)求處理結(jié)束后,A需要調(diào)用
    模塊B,C,D的對(duì)應(yīng)請(qǐng)求處理,這時(shí)B請(qǐng)求處理需要100ms,C 需要200ms,D 需要400ms。這樣一來,總體一個(gè)請(qǐng)求的總時(shí)長為
    20 + 100 + 200 + 400 = 720 ms 遠(yuǎn)遠(yuǎn)大于模塊A的請(qǐng)求處理時(shí)間。另一方面,模塊B 模塊C 模塊D之間 并沒有順序關(guān)系。
    這時(shí)可以引入消息隊(duì)列 ,模塊A在請(qǐng)求處理結(jié)束后,將自己的數(shù)據(jù)發(fā)送給消息隊(duì)列MQ,由B,C ,D去消息隊(duì)列獲取數(shù)據(jù),自行處理
    模塊A在處理完成后直接返回給客戶端處理結(jié)果,而不需要等待B,C,D處理結(jié)束,如此一來,一個(gè)請(qǐng)求處理的就只需要計(jì)算
    模塊A的處理時(shí)間=20ms,大大提高了用戶體驗(yàn)。


    異步
  3. 削峰
    如果一個(gè)系統(tǒng)只能一秒鐘處理5000個(gè)請(qǐng)求(MySQL一般只能2000QPS),而在特殊時(shí)期就只要1個(gè)小時(shí)的時(shí)間段內(nèi),請(qǐng)求量暴漲,一秒鐘來了1W個(gè)請(qǐng)求。從而系統(tǒng)會(huì)之間宕機(jī)。但這種情況可能在平時(shí)不會(huì)發(fā)生,不需要升級(jí)相應(yīng)的服務(wù)器配置。
    問題在于在1小時(shí)的時(shí)間段,如何把請(qǐng)求從10000QPS 下降到5000QPS 使得系統(tǒng)能夠正常運(yùn)轉(zhuǎn)而不發(fā)生宕機(jī)。
    這時(shí)候可以引入消息隊(duì)列,假設(shè)模塊A負(fù)責(zé)處理請(qǐng)求,模塊B負(fù)責(zé)將數(shù)據(jù)持久化到數(shù)據(jù)庫。模塊A在接受到請(qǐng)求時(shí),把請(qǐng)求交給消息隊(duì)列,由消息隊(duì)列來緩解對(duì)應(yīng)的請(qǐng)求壓力,類似于Buffer建立一個(gè)緩沖區(qū),模塊B根據(jù)自己的請(qǐng)求處理速度去到消息隊(duì)列中去消費(fèi)數(shù)據(jù)。這樣一來就解決了對(duì)應(yīng)特定時(shí)間段的削峰問題。


    削峰
  4. 要結(jié)合實(shí)際項(xiàng)目來說明:
    體現(xiàn)上述的三個(gè)點(diǎn)。

2. MQ有什么缺點(diǎn)?

任何技術(shù)都是一把雙刃劍,在引入消息隊(duì)列的同時(shí)必定也會(huì)伴隨著相應(yīng)的問題,正如《人月神話》中所說,沒有銀彈。
消息隊(duì)列的引入會(huì)帶來3個(gè)核心的問題:

  1. 系統(tǒng)可靠性降低
    在引入MQ時(shí),MQ作為了中間層,這就使得模塊與對(duì)應(yīng)的MQ是緊耦合的關(guān)系。一旦MQ宕機(jī),下游服務(wù)即使正常運(yùn)行,但整個(gè)系統(tǒng)卻無法使用。這個(gè)問題的解決方式是 MQ的高可用,使得MQ高可用,不會(huì)那么容易宕機(jī)達(dá)到5個(gè)9。
    3dFX8A.png
  2. 系統(tǒng)復(fù)雜度提高
    引入MQ,需要考慮的問題變復(fù)雜,隨之而來的問題是
    1. 消息丟了怎么辦
    2. 重復(fù)消費(fèi)消息問題
    3. 消息的順序問題
      這幾個(gè)問題都有對(duì)應(yīng)的解決方案。
      3dFjgI.png
  3. 處理結(jié)果的最終一致性問題
    引入MQ會(huì)導(dǎo)致處理結(jié)果的最終一致性問題,因?yàn)槟KA與其他模塊之間解耦,從而模塊A不知道其他模塊的處理結(jié)果
    這就導(dǎo)致模塊A以為處理結(jié)果OK,但實(shí)際上可能模塊B處理結(jié)果失敗,這也是異步化所帶來的最終一致性問題。
    3dFvvt.png

3. 你都了解過哪些MQ? 他們之間有什么區(qū)別嗎?

這個(gè)問題可以延伸為技術(shù)選型問題,關(guān)于這個(gè)問題可以認(rèn)為憑什么你選擇了這個(gè)MQ,而沒有選擇其他MQ。對(duì)應(yīng)可以擴(kuò)展為spring-security 與shiro 都是安全框架都實(shí)現(xiàn)了Oauth2協(xié)議,而且shiro是輕量級(jí)的,為什么你選擇了Spring-Security這個(gè)安全框架這個(gè)問題比較考察技術(shù)廣度。

首先我了解過的MQ有:activeMQ,RabbitMQ,RocketMQ,Kafka

activeMQ RabbitMQ RocketMQ Kafka
并發(fā)量 萬級(jí) 萬級(jí) 十萬級(jí) 萬級(jí)
處理時(shí)長 毫秒 微妙 毫秒 毫秒
開發(fā)語言 Java ErLang Java Java Scala
功能完備 完備 完備 且提供了插件與管理界面 完備 完備
常用場(chǎng)景 小型項(xiàng)目demo * * 大數(shù)據(jù)領(lǐng)域日志處理、實(shí)時(shí)計(jì)算
社區(qū)活躍度 較低

activeMQ社區(qū)活躍度較低,不建議使用。
RabbitMQ社區(qū)活躍度高,更新版本頻繁,但使用開發(fā)語言為ErLang , 當(dāng)有定制化需求無法進(jìn)行擴(kuò)展
RocketMQ 阿里出品,但存在著后續(xù)項(xiàng)目不更新情況,這就使得企業(yè)自行維護(hù)相應(yīng)的功能或者定制化功能
Kafka 大數(shù)據(jù)領(lǐng)域 主要用來進(jìn)行實(shí)時(shí)計(jì)算,日志采集的消息隊(duì)列,功能相比于其他MQ少,但是kafka是大數(shù)據(jù)領(lǐng)域公認(rèn)的消息隊(duì)列
如果對(duì)功能有要求,小公司可以選擇RabbitMQ, 有技術(shù)團(tuán)隊(duì)的大公司可以使用RocketMQ,大數(shù)據(jù)生態(tài)為了與其他組件配合所以使用Kafka

4. 如何保證MQ的高可用

  1. RabbitMQ的HA
    RabbitMQ的解決方式為=>集群模式 + 鏡像
    3dkDGd.png

    普通集群模式:
    queue創(chuàng)建之后,如果沒有其它policy(策略),則queue就會(huì)按照普通模式集群。對(duì)于Queue來說,消息實(shí)體只存在于其中一個(gè)節(jié)點(diǎn),A、B兩個(gè)節(jié)點(diǎn)僅有相同的元數(shù)據(jù),即隊(duì)列結(jié)構(gòu),但隊(duì)列的元數(shù)據(jù)僅保存有一份,即創(chuàng)建該隊(duì)列的rabbitmq節(jié)點(diǎn)(A節(jié)點(diǎn)),當(dāng)A節(jié)點(diǎn)宕機(jī),你可以去其B節(jié)點(diǎn)查看,./rabbitmqctl list_queues發(fā)現(xiàn)該隊(duì)列已經(jīng)丟失,但聲明的exchange還存在。
    當(dāng)消息進(jìn)入A節(jié)點(diǎn)的Queue中后,consumer從B節(jié)點(diǎn)拉取時(shí),RabbitMQ會(huì)臨時(shí)在A、B間進(jìn)行消息傳輸,把A中的消息實(shí)體取出并經(jīng)過B發(fā)送給consumer,所以consumer應(yīng)平均連接每一個(gè)節(jié)點(diǎn),從中取消息。
    該模式存在一個(gè)問題就是當(dāng)A節(jié)點(diǎn)故障后,B節(jié)點(diǎn)無法取到A節(jié)點(diǎn)中還未消費(fèi)的消息實(shí)體。如果做了隊(duì)列持久化或消息持久化,那么得等A節(jié)點(diǎn)恢復(fù),然后才可被消費(fèi),并且在A節(jié)點(diǎn)恢復(fù)之前其它節(jié)點(diǎn)不能再創(chuàng)建A節(jié)點(diǎn)已經(jīng)創(chuàng)建過的持久隊(duì)列;如果沒有持久化的話,消息就會(huì)失丟。這種模式更適合非持久化隊(duì)列。
    只有該隊(duì)列是非持久的,客戶端才能重新連接到集群里的其他節(jié)點(diǎn),并重新創(chuàng)建隊(duì)列。
    假如該隊(duì)列是持久化的,那么唯一辦法是將故障節(jié)點(diǎn)恢復(fù)起來。
    鏡像集群模式:
    核心在于:鏡像集群會(huì)同步消息
    該模式解決了上述問題,其實(shí)質(zhì)和普通模式不同之處在于,消息實(shí)體會(huì)主動(dòng)在鏡像節(jié)點(diǎn)間同步,而不是在consumer取數(shù)據(jù)時(shí)臨時(shí)拉取。該模式帶來的副作用也很明顯,除了降低系統(tǒng)性能外,如果鏡像隊(duì)列數(shù)量過多,加之大量的消息進(jìn)入,集群內(nèi)部的網(wǎng)絡(luò)帶寬將會(huì)被這種同步通訊大大消耗掉。所以在對(duì)可靠性要求較高的場(chǎng)合中適用,
    一個(gè)隊(duì)列想做成鏡像隊(duì)列,需要先設(shè)置policy,然后客戶端創(chuàng)建隊(duì)列的時(shí)候,rabbitmq集群根據(jù)“隊(duì)列名稱”自動(dòng)設(shè)置是普通集群模式或鏡像隊(duì)列。但這不是分布式存儲(chǔ),而是多節(jié)點(diǎn)主備存儲(chǔ)。相比于Kafka的分布式架構(gòu)會(huì)多消耗資源。
  2. Kafka實(shí)現(xiàn)高可用
    3dkrRA.png

    Kafka的高可用主要是通過Kafka通過把每個(gè)Topic中的消息分成多個(gè)Partition,每個(gè)Partition做一個(gè)集群。而每一個(gè)Partition內(nèi)部集群通過選舉得到一個(gè)leader,其他是follower,leader復(fù)制消息的讀寫,并把消息復(fù)制到follower。Kafka在發(fā)送消息時(shí),只有當(dāng)每個(gè)partition的follower復(fù)制到了消息才確認(rèn)消息已經(jīng)被存儲(chǔ)。

5. 如何保證消息不會(huì)丟失?

  1. RabbitMQ方式
    1.1 生產(chǎn)者方面
    • 開啟RabbitMQ的事務(wù)方式txSelect()。當(dāng)生產(chǎn)者寫入消息失敗時(shí),采取重試機(jī)制。但這個(gè)寫入是同步的。
    • 使用confirm方式: 其中confirm也可以使用三種方式:
    • 普通confirm
    • 批量confirm
    • 異步confirm 設(shè)置監(jiān)聽器
    channel.confirmSelect();
    //普通confirm
    if (channel.waitForConfirms()) {
    System.out.println("消息發(fā)送成功" );
    }
    //批量confirm 失敗會(huì)報(bào)錯(cuò)
    channel.waitForConfirmsOrDie();
    //異步監(jiān)聽confirm
    channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("未確認(rèn)消息,標(biāo)識(shí):" + deliveryTag);
    }
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println(String.format("已確認(rèn)消息,標(biāo)識(shí):%d,多個(gè)消息:%b", deliveryTag, multiple));
    }
});

1.2 RabbitMQ方面
RabbitMQ在內(nèi)存緩存消息,當(dāng)MQ宕機(jī)時(shí),會(huì)發(fā)生消息丟失現(xiàn)象。這一點(diǎn)需要RabbitMQ將消息持久化到硬盤上。減小消息丟失的可能
1.3 消費(fèi)者方面
RabbitMQ默認(rèn)是autoAck=true,這樣就使得消費(fèi)者在接受到消息時(shí),立馬告知MQ我消費(fèi)了數(shù)據(jù),但還沒有來得及處理。所以需要把a(bǔ)utoAck=false,之后當(dāng)消息處理完成之后手動(dòng)提交。

2 kafka方面

  1. 生產(chǎn)端
    數(shù)據(jù)丟失發(fā)送在leader向follower同步消息的時(shí)候,leader宕機(jī)使得消息丟失。
    解決方案是設(shè)置4個(gè)參數(shù):
    1. replication.factor = n > 1 設(shè)置partition有多少個(gè)副本數(shù)
    2. kafka咋服務(wù)端設(shè)置mini.sync.replicas=1 即一個(gè)leader至少保證有一個(gè)follower存活
    3. producer在發(fā)送消息時(shí),設(shè)置acks=all 設(shè)置所有消息必須全部寫入replication后返回成功。
    4. retries=Integer.MAX 把重試次數(shù)調(diào)制最大
  2. 消費(fèi)端
    在消費(fèi)者端關(guān)閉自動(dòng)確認(rèn)消息,這樣需要手動(dòng)確認(rèn) 保證消息不會(huì)丟失。

6. 如何保證消息不會(huì)重復(fù)消費(fèi)?

  1. 在消費(fèi)者消費(fèi)消息時(shí),把對(duì)應(yīng)的唯一值放入HashSet 或者Redis來避免同一條消息消費(fèi)多次
  2. 使用數(shù)據(jù)庫的唯一鍵約束來報(bào)錯(cuò)處理

7. 如何保證消費(fèi)者消費(fèi)消息時(shí)消息的順序性?

消息隊(duì)列本身就是為了把多個(gè)任務(wù)解耦并行化處理,如果要保證消息的順序消息,實(shí)際上就是取消并行處理,改成串行處理。

  1. RabbitMQ
    將一組消息放入同一個(gè)queue,這樣就只會(huì)有一個(gè)consumer來消費(fèi)這個(gè)queue中的數(shù)據(jù),在consumer內(nèi)部采用隊(duì)列的方式來處理順序消息
  2. Kafka
    生產(chǎn)者寫消息時(shí),每次寫入一個(gè)topic,將partition對(duì)應(yīng)key值修改同一key,這樣消息只會(huì)進(jìn)入一個(gè)partition,也只有一個(gè)消費(fèi)者在進(jìn)行消費(fèi),之后也采用內(nèi)存隊(duì)列的方式順序處理消息。

8. 消息隊(duì)列積壓太多,目前消費(fèi)者處理太慢,如何改進(jìn)?


目前的消費(fèi)者難以在短時(shí)間內(nèi)處理這么多條消息,考慮引入多個(gè)消費(fèi)者,但引入這多個(gè)消費(fèi)者只用于消費(fèi)當(dāng)前消息,并不是長期使用。假定長期使用的消費(fèi)者為A、B、C 訂閱的Topic是Order。新申請(qǐng)3 * 10~20 = 30~60個(gè)機(jī)器 作為新的消費(fèi)者組GroupNew。 GroupNew 訂閱新的Topic - OrderFast,從這個(gè)topic中獲取消息并消費(fèi)。將原有長期使用的消費(fèi)者組修改代碼,不處理消息 直接將消息寫入到新的Topic - OrderFast

9. 消息隊(duì)列的機(jī)器磁盤快被消息寫滿了怎么辦?

  1. 可以采取上述方案,不同的是寫入到另外一臺(tái)MQ中,而不是在本機(jī)的Topic
  2. 下一個(gè)消息直接丟棄不處理,到消息隊(duì)列機(jī)器恢復(fù)之后,將生產(chǎn)者與消費(fèi)者之間通過代碼查詢出對(duì)應(yīng)的缺失部分,再進(jìn)行補(bǔ)償式操作。

10. 消息隊(duì)列消費(fèi)消息太慢,消息過期失效怎么辦?

  1. 生產(chǎn)環(huán)境設(shè)置消息不過期
  2. 如果已經(jīng)過期失效,那么需要查出過期失效的消息,重新進(jìn)行補(bǔ)償式操作

本文由博客一文多發(fā)平臺(tái) OpenWrite 發(fā)布!

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

相關(guān)閱讀更多精彩內(nèi)容

  • 學(xué)習(xí) 1、MQ消息隊(duì)列分類有哪些? 消息隊(duì)列分類:點(diǎn)對(duì)點(diǎn)和發(fā)布/訂閱兩種: 1.1、點(diǎn)對(duì)點(diǎn): 支持此模式:rabb...
    Hibug閱讀 1,189評(píng)論 0 4
  • 1. 項(xiàng)目中為什么使用mq (1) 解耦 一個(gè)模塊, 調(diào)用其他多個(gè)模塊的接口, 調(diào)用過程很復(fù)雜, 但又不是必須同步...
    lj72808up閱讀 414評(píng)論 0 0
  • (1)為什么使用消息隊(duì)列??? 其實(shí)就是問問你消息隊(duì)列都有哪些使用場(chǎng)景,然后你項(xiàng)目里具體是什么場(chǎng)景,說說你在這個(gè)場(chǎng)景...
    Mybenita閱讀 1,042評(píng)論 0 0
  • 每個(gè)人面臨的選擇很多,重要的是哪個(gè)選擇是最適合自己的,當(dāng)做出選擇時(shí)我們要考慮它的價(jià)值和意義所在,總要不斷去試錯(cuò),才...
    莊語閱讀 215評(píng)論 0 1
  • 跟最好的朋友吵架是一件很無能為力的事情。 不能像18歲那樣幼稚,也不夠30歲的成熟。 不能講落下的眼淚,也不想各自...
    唐小茴閱讀 157評(píng)論 0 0

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