消息中間件面試題(網上資料整理)

參考:消息中間件面試題:消息隊列的優缺點,區別
參考:消息中間件面試題:消息丟失怎麼辦?
參考:RabbitMQ和Kafka的高可用
參考:中華石杉的架構
參考:阿里Java面試題剖析:如何解決消息隊列的延時以及過期失效問題?

1.爲什麼使用消息隊列

消息隊列常見的使用場景吧,其實場景有很多,但是比較核心的有 3 個:解耦、異步、削峯。

1.解耦

A 系統發送數據到 BCD 三個系統,通過接口調用發送。如果 E 系統也要這個數據呢?那如果 C 系統現在不需要了呢?A 系統負責人幾乎崩潰…
在這裏插入圖片描述

在這個場景中,A 系統跟其它各種亂七八糟的系統嚴重耦合,A 系統產生一條比較關鍵的數據,很多系統都需要 A 系統將這個數據發送過來。A 系統要時時刻刻考慮 BCDE 四個系統如果掛了該咋辦?要不要重發,要不要把消息存起來?頭髮都白了啊!

如果使用 MQ,A 系統產生一條數據,發送到 MQ 裏面去,哪個系統需要數據自己去 MQ 裏面消費。如果新系統需要數據,直接從 MQ 裏消費即可;如果某個系統不需要這條數據了,就取消對 MQ 消息的消費即可。這樣下來,A 系統壓根兒不需要去考慮要給誰發送數據,不需要維護這個代碼,也不需要考慮人家是否調用成功、失敗超時等情況。

在這裏插入圖片描述
總結:通過一個 MQ,Pub/Sub 發佈訂閱消息這麼一個模型,A 系統就跟其它系統徹底解耦了。

2.異步

再來看一個場景,A 系統接收一個請求,需要在自己本地寫庫,還需要在 BCD 三個系統寫庫,自己本地寫庫要 3ms,BCD 三個系統分別寫庫要 300ms、450ms、200ms。最終請求總延時是 3 + 300 + 450 + 200 = 953ms,接近 1s,用戶感覺搞個什麼東西,慢死了慢死了。用戶通過瀏覽器發起請求,等待個 1s,這幾乎是不可接受的。
在這裏插入圖片描述
一般互聯網類的企業,對於用戶直接的操作,一般要求是每個請求都必須在 200 ms 以內完成,對用戶幾乎是無感知的。

如果使用 MQ,那麼 A 系統連續發送 3 條消息到 MQ 隊列中,假如耗時 5ms,A 系統從接受一個請求到返回響應給用戶,總時長是 3 + 5 = 8ms,對於用戶而言,其實感覺上就是點個按鈕,8ms 以後就直接返回了,爽!網站做得真好,真快!

在這裏插入圖片描述

3.削峯

一般的 MySQL,扛到每秒 2k 個請求就差不多了,如果每秒請求到 5k 的話,可能就直接把 MySQL 給打死了,導致系統崩潰,用戶也就沒法再使用系統了。

如果使用 MQ,每秒 5k 個請求寫入 MQ,A 系統每秒鐘最多處理 2k 個請求,因爲 MySQL 每秒鐘最多處理 2k 個。A 系統從 MQ 中慢慢拉取請求,每秒鐘就拉取 2k 個請求,不要超過自己每秒能處理的最大請求數量就 ok,這樣下來,哪怕是高峯期的時候,A 系統也絕對不會掛掉。而 MQ 每秒鐘 5k 個請求進來,就 2k 個請求出去,結果就導致在中午高峯期(1 個小時),可能有幾十萬甚至幾百萬的請求積壓在 MQ 中。

這個短暫的高峯期積壓是 ok 的,因爲高峯期過了之後,每秒鐘就 50 個請求進 MQ,但是 A 系統依然會按照每秒 2k 個請求的速度在處理。所以說,只要高峯期一過,A 系統就會快速將積壓的消息給解決掉。

在這裏插入圖片描述

2.消息隊列有什麼缺點

1.系統可用性降低

系統引入的外部依賴越多,越容易掛掉。

2.系統複雜度提高

硬生生加個 MQ 進來,你怎麼[保證消息沒有重複消費]?怎麼[處理消息丟失的情況]?怎麼保證消息傳遞的順序性?

3.一致性問題

A 系統處理完了直接返回成功了,人都以爲你這個請求就成功了;但是問題是,要是 BCD 三個系統那裏,BD 兩個系統寫庫成功了,結果 C
系統寫庫失敗了,咋整?你這數據就不一致了。

3.RabbitMQ的高可用

RabbitMQ是基於主從複製來實現高可用的,不支持分佈式。既然是主從複製,那麼肯定就不是單臺機器能保證的,所以是通過集羣來保證高可用。而RabbitMQ的集羣模式有兩種:普通集羣和鏡像集羣。真正實現高可用的是鏡像集羣模式。

1.普通集羣模式

在這裏插入圖片描述
該集羣模式不能保證高可用,它的作用只是提高了系統的吞吐量,同一個隊列的消息,可以有多個消費者去消費。而且它的缺點是:在實例之間會產生網絡傳輸,增加系統開銷。

而且如果那個存有queue的真實數據的實例宕機了,會導致接下來其他實例都無法拉取數據;如果沒有開啓消息的持久化會丟失消息;就算開啓了消息的持久化,消息不一定會丟,但是也要等這個實例恢復了,纔可以繼續拉取數據。

2.鏡像集羣模式

在這裏插入圖片描述
這種集羣模式是可以保證可用的,因爲每個實例都存有完整的數據,就算其中的某一個實例宕機了,也只是這一個實例不能提供服務,其他的實例都能繼續提供服務。

缺點

  • 性能消耗太大,所有機器都要進行消息的同步,導致網絡壓力和消耗很大。
  • 沒有擴展性可言,如果有一個queue負載很重,就算加了機器,新增的機器還是包含了這個queue的所有數據,並沒有辦法擴展queue。

實際上RabbitMQ並不是分佈式消息隊列,他就是傳統的消息隊列,只不過提供了一些集羣、HA的機制而已,因爲無論如何配置,rabbitmq一個queue的數據就存放在一個節點裏面,鏡像集羣下,也是每個節點都放這個queue的全部數據。

3 開啓鏡像集羣

在控制檯新增一個鏡像集羣模式的策略,指定的時候可以要求數據同步到所有節點,也可以要求同步到指定節點,然後在創建queue的時候,應用這個策略,就會自動將數據同步到其他的節點上面去了。

4.Kafka的高可用

1.三大組件

Broker
Kafka集羣包含一個或多個服務器,這種服務器被稱爲broker [5]
Topic
每條發佈到Kafka集羣的消息都有一個類別,這個類別被稱爲Topic。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存於一個或多個broker上但用戶只需指定消息的Topic即可生產或消費數據而不必關心數據存於何處)
Partition
Partition是物理上的概念,每個Topic包含一個或多個Partition.

在這裏插入圖片描述

2.架構組成

Kafka的一個基本架構:多個broker組成,一個broker是一個節點;一個topic可以劃分成多個partition,每個partition可以存在於不同的broker上面,每個partition存放一部分數據。這是天然的分佈式消息隊列。

Kafka在0.8以前是沒有HA機制的,也就是說任何一個broker宕機了,那個broker上的partition就丟了,沒法讀也沒法寫,沒有什麼高可用可言。

Kafka在0.8之後,提過了HA機制,也就是replica副本機制。每個partition的數據都會同步到其他機器上,形成自己的replica副本。然後所有的replica副本會選舉一個leader出來,那麼生產者消費者都和這個leader打交道,其他的replica就是follower。寫的時候,leader會把數據同步到所有follower上面去,讀的時候直接從leader上面讀取即可。

爲什麼只能讀寫leader
因爲要是你可以隨意去讀寫每個follower,那麼就要關心數據一致性問題,系統複雜度太高,容易出問題。kafka會均勻度講一個partition的所有數據replica分佈在不同的機器上,這樣就可以提高容錯性。
這樣就是高可用了,因爲如果某個broker宕機 了,沒事兒,那個broker的partition在其他機器上有副本,如果這上面有某個partition的leader,那麼此時會重新選舉出一個新的leader出來,繼續讀寫這個新的leader即可。

寫消息
寫數據的時候,生產者就寫向leader,然後leader將數據落到磁盤上之後,接着其他follower自己主動從leader來pull數據。一旦所有follower同步好了數據,就會發送ack給leader,leader收到了所有的follower的ack之後,就會返回寫成功的消息給消息生產者。(這只是一種模式,可以調整)。
讀數據
消費數據的時候,只會從leader進行消費。但是隻有一個消息已經被所有follower都同步成功返回ack的時候,這個消息纔會被消費者讀到

5.消息丟失怎麼辦?

1.RabbitMQ的可靠性傳輸

在這裏插入圖片描述

1.生產者沒有成功把消息發送到MQ

丟失的原因
因爲網絡傳輸的不穩定性,當生產者在向MQ發送消息的過程中,MQ沒有成功接收到消息,但是生產者卻以爲MQ成功接收到了消息,不會再次重複發送該消息,從而導致消息的丟失。

事務機制解決
RabbitMQ 提供了事務功能,生產者發送數據之前開啓 RabbitMQ 事務channel.txSelect,然後發送消息,如果消息沒有成功被 RabbitMQ 接收到,那麼生產者會收到異常報錯,此時就可以回滾事務channel.txRollback,然後重試發送消息;如果收到了消息,那麼可以提交事務channel.txCommit。

// 開啓事務
	channel.txSelect
	try {
	    // 這裏發送消息
	} catch (Exception e) {
	    channel.txRollback
	 
	    // 這裏再次重發這條消息
	}
	// 提交事務
	channel.txCommit

confirm機制
RabbitMQ可以開啓 confirm 模式,在生產者那裏設置開啓 confirm 模式之後,生產者每次寫的消息都會分配一個唯一的 id,如果消息成功寫入 RabbitMQ 中,RabbitMQ 會給生產者回傳一個 ack 消息,告訴你說這個消息 ok 了。如果 RabbitMQ 沒能處理這個消息,會回調你的一個 nack 接口,告訴你這個消息接收失敗,生產者可以發送。而且你可以結合這個機制自己在內存裏維護每個消息 id 的狀態,如果超過一定時間還沒接收到這個消息的回調,那麼可以重發。

注意
RabbitMQ的事務機制是同步的,很耗型能,會降低RabbitMQ的吞吐量。confirm機制是異步的,生成者發送完一個消息之後,不需要等待RabbitMQ的回調,就可以發送下一個消息,當RabbitMQ成功接收到消息之後會自動異步的回調生產者的一個接口返回成功與否的消息。

2.RabbitMQ接收到消息之後丟失了消息

丟失的原因
RabbitMQ接收到生產者發送過來的消息,是存在內存中的,如果沒有被消費完,此時RabbitMQ宕機了,那麼再次啓動的時候,原來內存中的那些消息都丟失了。

持久化解決
開啓RabbitMQ的持久化。當生產者把消息成功寫入RabbitMQ之後,RabbitMQ就把消息持久化到磁盤。結合上面的說到的confirm機制,只有當消息成功持久化磁盤之後,纔會回調生產者的接口返回ack消息,否則都算失敗,生產者會重新發送。存入磁盤的消息不會丟失,就算RabbitMQ掛掉了,重啓之後,他會讀取磁盤中的消息,不會導致消息的丟失。
持久化設置

  • 第一點是創建 queue 的時候將其設置爲持久化,這樣就可以保證 RabbitMQ 持久化 queue 的元數據,但是它是不會持久化 queue 裏的數據的。
  • 第二個是發送消息的時候將消息的 deliveryMode 設置爲 2,就是將消息設置爲持久化的,此時 RabbitMQ 就會將消息持久化到磁盤上去。

注意
持久化要起作用必須同時設置這兩個持久化才行,RabbitMQ 哪怕是掛了,再次重啓,也會從磁盤上重啓恢復 queue,恢復這個 queue 裏的數據。

3. 消費者弄丟了消息

丟失的原因
如果RabbitMQ成功的把消息發送給了消費者,那麼RabbitMQ的ack機制會自動的返回成功,表明發送消息成功,下次就不會發送這個消息。但如果就在此時,消費者還沒處理完該消息,然後宕機了,那麼這個消息就丟失了。

解決的辦法
必須關閉 RabbitMQ 的自動 ack,可以通過一個 api 來調用就行,然後每次你自己代碼裏確保處理完的時候,再在編碼中 ack 一下。這樣的話,如果你還沒處理完, RabbitMQ 就不會有 ack 。此時 RabbitMQ 會把這個消費分配給別的 consumer 去處理,消息是不會丟的。(我的理解是讓消費者消費完成之後返回處理結果,如果超時、失敗表示失敗,需要分發給其他的消費者執行)
在這裏插入圖片描述

2.Kafka的可靠性傳輸

在這裏插入圖片描述

1.生產者丟失消息

生產者把消息發送到了Kafka,但是Kafka還沒保存到消息,就宕機了,導致了消息的丟失。但是如果我們爲了不讓Kafka丟失消息,進行了上面這樣4個參數的配置,那麼就不會出現生產者丟失消息了(見第二點)。因爲消息一定要存到了副本上纔會證明消息發送成功,否則生產者會一直重試。

2.Kafka丟失消息

丟失的原因
在Kafka的高可用集羣環境中,生產者和消費者都是跟leader節點交流的,leader接收到生產者的消息之後,會同步到其他的follower中。如果leader在接收到消息之後,同步消息到follower的操作還沒完成,此時leader就宕機了。那麼就會從follower中選出一個新的leader,而這個leader中就會缺少沒有同步的那些數據,消費者也就無法消費到,這就造成了消息的丟失。

解決的辦法

  • 給 topic 設置 replication.factor 參數:這個值必須大於 1,要求每個 partition 必須有至少 2 個副本。
  • 在 Kafka 服務端設置 min.insync.replicas 參數:這個值必須大於 1,這個是要求一個 leader 至少感知到有至少一個 follower 還跟自己保持聯繫,沒掉隊,這樣才能確保 leader 掛了還有一個 follower 吧。
  • 在 producer 端設置 acks=all:這個是要求每條數據,必須是寫入所有 replica 之後,才能認爲是寫成功了。
  • 在 producer 端設置 retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這裏了。

3.消費者端丟失消息

丟失的原因

唯一可能導致消費者弄丟數據的情況,就是說,你消費到了這個消息,然後消費者那邊自動提交了 offset,讓 Kafka 以爲你已經消費了這個消息,但其實你纔剛準備處理這個消息,你還沒處理,你自己就掛了,此時這條消息就丟咯。

解決方案
Kafka 會自動提交 offset,那麼只要關閉自動提交 offset,在處理完之後自己手動提交 offset,就可以保證數據不會丟。

在這裏插入圖片描述

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

業務場景是這樣的:我們需要根據mysql的binlog日誌同步一個數據庫的數據到另一個庫中,加如在binlog中對同一條數據做了insert,update,delete操作,我們往MQ順序寫入了insert,update,delete操作的三條消息,那麼根據分析,最終同步到另一個庫中,這條數據是被刪除了的。但是,如果這三條消息不是按照insert,update,delete順序被消費,而是按照delete,insert,update的順序被消費,那麼最終這條數據是會保存到新庫中的。這就導致了數據錯亂了。下面分別講解下RabbitMQ和Kafka是如何保證消息的順序性。

1.RabbitMQ出現消息順序性錯亂的情況

queue(隊列)中的消息只能被一個消費者所消費,然後消費者在消費消息的過程中是無序的。如上圖所示,如果按照BAC的消費順序,那麼最終數據庫中是被保存這條數據的。這和我們預期的結果不符,如果這樣的情況很多,那麼就造成了數據庫中的數據完成不對,同步工作也是白費了。
在這裏插入圖片描述

2.RabbitMQ保證消息順序性

RabbitMQ保證消息的順序性,就是拆分多個 queue,每個 queue 對應一個 consumer(消費者),就是多一些 queue 而已,確實是麻煩點;或者就一個 queue 但是對應一個 consumer,然後這個 consumer 內部用內存隊列做排隊,然後分發給底層不同的 worker 來處理。

在這裏插入圖片描述

3.Kafka出現消息順序性錯亂的情況

首先要說明一些Kafka的特性:比如一個topic,有三個partition,生產者在寫的時候可以指定一個key,假設以訂單id作爲key,那麼這個訂單key相關的數據,一定會被分發到同一個partition中去,並且是有序的。同時消費者從 partition 中取出來數據的時候,也一定是有順序的。
。從Kafka的特性可以知道,這三條消息會分到一個partition中,一個消費者去消費這個partition中的數據,這個時候都能說是有序的。但是,單線程的情況下,吞吐量很低,一秒鐘可能只能處理十多條消息,所以一般會採用多線程去提高吞吐量。線程之間的執行順序和速度就不能保證有序了,如果thread1是拿到的delete,然後執行了,thread2拿到的消息是insert,然後執行,thread3拿到的消息是update,然後執行,所以最終這個訂單的數據是會存到新庫中的。這和原有的業務需求是不符合的。

在這裏插入圖片描述

4.Kafka保證消息的順序性

每個線程都只從一個內存隊列中取數據,都相關的數據都存到同一個內存隊列中,就保證了數據的有序性。
在這裏插入圖片描述

7.如何保證消息冪等性

1.MQ出現非冪等性的情況

1.生成者重複發送消息給MQ

生成者把消息發送給MQ之後,MQ收到消息在給生產者返回ack的時候,網絡中斷了。這時MQ明明已經接收到了消息,但是生產者沒接收到確定消息,就會認爲MQ沒有接收到消息。因此,在網絡重新連接後,生產者會把已經發送的消息再次發送到MQ,如果MQ沒有去重措施的話,那麼就接收到了重複的消息。

2.MQ重複發送消息給消費者

消費者從MQ中拉取消息進行消費,當消費者已經消費了消息但還沒向MQ返回ack的時候,消費者宕機或者網絡斷開了。所以消費者成功消費了消息的情況,MQ並不知道。當消費者重啓或網絡重連後,消費者再次去請求MQ拉取消息的時候,MQ會把已經消費的消息再次發送給消費者,如果消費者沒有去重就直接消費,那麼就會造成重複消費的情況。便會造成數據的不一致。

2. Kafka重複消費的場景

在這裏插入圖片描述

3.保證消息冪等性的辦法

1.生成者不重複發送消息到MQ

mq內部可以爲每條消息生成一個全局唯一、與業務無關的消息id,當mq接收到消息時,會先根據該id判斷消息是否重複發送,mq再決定是否接收該消息。

2.消費者不重複消費

消費者怎麼保證不重複消費的關鍵在於消費者端做控制,因爲MQ不能保證不重複發送消息,所以應該在消費者端控制:即使MQ重複發送了消息,消費者拿到了消息之後,要判斷是否已經消費過,如果已經消費,直接丟棄。所以根據實際業務情況,有下面幾種方式:

  • 如果從MQ拿到數據是要存到數據庫,那麼可以根據數據創建唯一約束,這樣的話,同樣的數據從MQ發送過來之後,當插入數據庫的時候,會報違反唯一約束,不會插入成功的。(或者可以先查一次,是否在數據庫中已經保存了,如果能查到,那就直接丟棄就好了)。

  • 讓生產者發送消息時,每條消息加一個全局的唯一id,然後消費時,將該id保存到redis裏面。消費時先去redis裏面查一下有麼有,沒有再消費。(其實原理跟第一點差不多)。

  • 如果拿到的數據是直接放到redis的set中的話,那就不用考慮了,因爲set集合就是自動有去重的。

8.大量消息在 mq 裏積壓了幾個小時了還沒解決

一個消費者一秒是 1000 條,一秒 3 個消費者是 3000 條,一分鐘就是 18 萬條。所以如果你積壓了幾百萬到上千萬的數據,即使消費者恢復了,也需要大概 1 小時的時間才能恢復過來。

一般這個時候,只能臨時緊急擴容了,具體操作步驟和思路如下:
先修復 consumer 的問題,確保其恢復消費速度,然後將現有 consumer 都停掉。
新建一個 topic,partition 是原來的 10 倍,臨時建立好原先 10 倍的 queue 數量。
然後寫一個臨時的分發數據的 consumer 程序,這個程序部署上去消費積壓的數據,消費之後不做耗時的處理,直接均勻輪詢寫入臨時建立好的 10 倍數量的 queue。
接着臨時徵用 10 倍的機器來部署 consumer,每一批 consumer 消費一個臨時 queue 的數據。這種做法相當於是臨時將 queue 資源和 consumer 資源擴大 10 倍,以正常的 10 倍速度來消費數據。
等快速消費完積壓數據之後,得恢復原先部署的架構,重新用原先的 consumer 機器來消費消息。

9.mq 中的消息過期失效了

假設你用的是 RabbitMQ,RabbtiMQ 是可以設置過期時間的,也就是 TTL。如果消息在 queue 中積壓超過一定的時間就會被 RabbitMQ 給清理掉,這個數據就沒了。那這就是第二個坑了。這就不是說數據會大量積壓在 mq 裏,而是大量的數據會直接搞丟。

這個情況下,就不是說要增加 consumer 消費積壓的消息,因爲實際上沒啥積壓,而是丟了大量的消息。我們可以採取一個方案,就是批量重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄數據了,然後等過了高峯期以後,比如大家一起喝咖啡熬夜到晚上12點以後,用戶都睡覺了。這個時候我們就開始寫程序,將丟失的那批數據,寫個臨時程序,一點一點的查出來,然後重新灌入 mq 裏面去,把白天丟的數據給他補回來。也只能是這樣了。

假設 1 萬個訂單積壓在 mq 裏面,沒有處理,其中 1000 個訂單都丟了,你只能手動寫程序把那 1000 個訂單給查出來,手動發到 mq 裏去再補一次。

10.mq 都快寫滿了

如果消息積壓在 mq 裏,你很長時間都沒有處理掉,此時導致 mq 都快寫滿了,咋辦?這個還有別的辦法嗎?沒有,誰讓你第一個方案執行的太慢了,你臨時寫程序,接入數據來消費,消費一個丟棄一個,都不要了,快速消費掉所有的消息。然後走第二個方案,到了晚上再補數據吧。

11.如果讓你寫一個消息隊列,該如何進行架構設計?

其實回答這類問題,說白了,不求你看過那技術的源碼,起碼你要大概知道那個技術的基本原理、核心組成部分、基本架構構成,然後參照一些開源的技術把一個系統設計出來的思路說一下就好。

比如說這個消息隊列系統,我們從以下幾個角度來考慮一下:

  • 首先這個 mq 得支持可伸縮性吧,就是需要的時候快速擴容,就可以增加吞吐量和容量,那怎麼搞?設計個分佈式的系統唄,參照一下 kafka 的設計理念,broker -> topic -> partition,每個 partition 放一個機器,就存一部分數據。如果現在資源不夠了,簡單啊,給 topic 增加 partition,然後做數據遷移,增加機器,不就可以存放更多數據,提供更高的吞吐量了?

  • 其次你得考慮一下這個 mq 的數據要不要落地磁盤吧?那肯定要了,落磁盤才能保證別進程掛了數據就丟了。那落磁盤的時候怎麼落啊?順序寫,這樣就沒有磁盤隨機讀寫的尋址開銷,磁盤順序讀寫的性能是很高的,這就是 kafka 的思路。

  • 其次你考慮一下你的 mq 的可用性啊?這個事兒,具體參考之前可用性那個環節講解的 kafka 的高可用保障機制。多副本 -> leader & follower -> broker 掛了重新選舉 leader 即可對外服務。

  • 能不能支持數據 0 丟失啊?可以的,參考我們之前說的那個 kafka 數據零丟失方案。

mq 肯定是很複雜的,面試官問你這個問題,其實是個開放題,他就是看看你有沒有從架構角度整體構思和設計的思維以及能力。確實這個問題可以刷掉一大批人,因爲大部分人平時不思考這些東西。

發佈了84 篇原創文章 · 獲贊 15 · 訪問量 3209
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章