消息隊列面試連環問:如何保證消息不丟失?處理重複消息?消息有序性?消息堆積處理?

你們好,我是 yes。java

最近我一直紮在消息隊列實現細節之中沒法自拔,已經寫了 3 篇Kafka源碼分析,還剩不少沒肝完。以前還存着RocketMQ源碼分析還沒整理。今兒暫時先跳出來盤一盤大方向上的消息隊列有哪些核心注意點。web

核心點有不少,爲了更貼合實際場景,我從常見的面試問題入手:面試

  • 如何保證消息不丟失?
  • 如何處理重複消息?
  • 如何保證消息的有序性?
  • 如何處理消息堆積?

固然在剖析這幾個問題以前須要簡單的介紹下什麼是消息隊列,消息隊列常見的一些基本術語和概念算法

接下來進入正文。數據庫

什麼是消息隊列

來看看維基百科怎麼說的,順帶學學英語這波不虧:後端

In computer science, message queues and mailboxes are software-engineering components typically used for inter-process communication (IPC), or for inter-thread communication within the same process. They use a queue for messaging – the passing of control or of content. Group communication systems provide similar kinds of functionality.緩存

翻譯一下:在計算機科學領域,消息隊列和郵箱都是軟件工程組件,一般用於進程間或同一進程內的線程通訊。它們經過隊列來傳遞消息-傳遞控制信息或內容,羣組通訊系統提供相似的功能。服務器

簡單的歸納下上面的定義:消息隊列就是一個使用隊列來通訊的組件微信

上面的定義沒有錯,但就如今而言咱們平常所說的消息隊列經常指代的是消息中間件,它的存在不只僅只是爲了通訊這個問題。網絡

爲何須要消息隊列

從本質上來講是由於互聯網的快速發展,業務不斷擴張,促使技術架構須要不斷的演進。

從之前的單體架構到如今的微服務架構,成百上千的服務之間相互調用和依賴。從互聯網初期一個服務器上有 100 個在線用戶已經很了不起,到如今坐擁10億日活的微信。咱們須要有一個「東西」來解耦服務之間的關係、控制資源合理合時的使用以及緩衝流量洪峯等等。

消息隊列就應運而生了。它經常使用來實現:異步處理、服務解耦、流量控制

異步處理

隨着公司的發展你可能會發現你項目的請求鏈路愈來愈長,例如剛開始的電商項目,能夠就是粗暴的扣庫存、下單。慢慢地又加上積分服務、短信服務等。這一路同步調用下來客戶可能等急了,這時候就是消息隊列登場的好時機。

調用鏈路長、響應就慢了,而且相對於扣庫存和下單,積分和短信不必這麼的 "及時"。所以只須要在下單結束那個流程,扔個消息到消息隊列中就能夠直接返回響應了。並且積分服務和短信服務能夠並行的消費這條消息。

能夠看出消息隊列能夠減小請求的等待,還能讓服務異步併發處理,提高系統整體性能

服務解耦

上面咱們說到加了積分服務和短信服務,這時候可能又要來個營銷服務,以後領導又說想作個大數據,又來個數據分析服務等等。

能夠發現訂單的下游系統在不斷的擴充,爲了迎合這些下游系統訂單服務須要常常地修改,任何一個下游系統接口的變動可能都會影響到訂單服務,這訂單服務組可瘋了,真 ·「核心」項目組

因此通常會選用消息隊列來解決系統之間耦合的問題,訂單服務把訂單相關消息塞到消息隊列中,下游系統誰要誰就訂閱這個主題。這樣訂單服務就解放啦!

流量控制

想必你們都聽過「削峯填谷」,後端服務相對而言都是比較「弱」的,由於業務較重,處理時間較長。像一些例如秒殺活動爆發式流量打過來可能就頂不住了。所以須要引入一箇中間件來作緩衝,消息隊列再適合不過了。

網關的請求先放入消息隊列中,後端服務盡本身最大能力去消息隊列中消費請求。超時的請求能夠直接返回錯誤。

固然還有一些服務特別是某些後臺任務,不須要及時地響應,而且業務處理複雜且流程長,那麼過來的請求先放入消息隊列中,後端服務按照本身的節奏處理。這也是很 nice 的。

上面兩種狀況分別對應着生產者生產過快和消費者消費過慢兩種狀況,消息隊列都能在其中發揮很好的緩衝效果。

注意

引入消息隊列當然有以上的好處,可是多引入一箇中間件系統的穩定性就降低一層,運維的難度擡高一層。所以要權衡利弊系統是演進的

消息隊列基本概念

消息隊列有兩種模型:隊列模型發佈/訂閱模型

隊列模型

生產者往某個隊列裏面發送消息,一個隊列能夠存儲多個生產者的消息,一個隊列也能夠有多個消費者, 可是消費者之間是競爭關係,即每條消息只能被一個消費者消費。

發佈/訂閱模型

爲了解決一條消息能被多個消費者消費的問題,發佈/訂閱模型就來了。該模型是將消息發往一個Topic即主題中,全部訂閱了這個 Topic 的訂閱者都能消費這條消息。

其實能夠這麼理解,發佈/訂閱模型等於咱們都加入了一個羣聊中,我發一條消息,加入了這個羣聊的人都能收到這條消息。那麼隊列模型就是一對一聊天,我發給你的消息,只能在你的聊天窗口彈出,是不可能彈出到別人的聊天窗口中的。

講到這有人說,那我一對一聊天對每一個人都發一樣的消息不就也實現了一條消息被多我的消費了嘛。

是的,經過多隊列全量存儲相同的消息,即數據的冗餘能夠實現一條消息被多個消費者消費。RabbitMQ 就是採用隊列模型,經過 Exchange 模塊來將消息發送至多個隊列,解決一條消息須要被多個消費者消費問題。

這裏還能看到假設羣聊裏除我以外只有一我的,那麼此時的發佈/訂閱模型和隊列模型其實就同樣了。

小結一下

隊列模型每條消息只能被一個消費者消費,而發佈/訂閱模型就是爲讓一條消息能夠被多個消費者消費而生的,固然隊列模型也能夠經過消息全量存儲至多個隊列來解決一條消息被多個消費者消費問題,可是會有數據的冗餘。

發佈/訂閱模型兼容隊列模型,即只有一個消費者的狀況下和隊列模型基本一致。

RabbitMQ 採用隊列模型,RocketMQKafka 採用發佈/訂閱模型。

接下來的內容都基於發佈/訂閱模型

經常使用術語

通常咱們稱發送消息方爲生產者 Producer,接受消費消息方爲消費者Consumer,消息隊列服務端爲Broker

消息從Producer發往BrokerBroker將消息存儲至本地,而後ConsumerBroker拉取消息,或者Broker推送消息至Consumer,最後消費。

爲了提升併發度,每每發佈/訂閱模型還會引入隊列或者分區的概念。即消息是發往一個主題下的某個隊列或者某個分區中。RocketMQ中叫隊列,Kafka叫分區,本質同樣。

例如某個主題下有 5 個隊列,那麼這個主題的併發度就提升爲 5 ,同時能夠有 5 個消費者並行消費該主題的消息。通常能夠採用輪詢或者 key hash 取餘等策略來將同一個主題的消息分配到不一樣的隊列中。

與之對應的消費者通常都有組的概念 Consumer Group, 即消費者都是屬於某個消費組的。一條消息會發往多個訂閱了這個主題的消費組。

假設如今有兩個消費組分別是Group 1Group 2,它們都訂閱了Topic-a。此時有一條消息發往Topic-a,那麼這兩個消費組都能接收到這條消息。

而後這條消息實際是寫入Topic某個隊列中,消費組中的某個消費者對應消費一個隊列的消息。

在物理上除了副本拷貝以外,一條消息在Broker中只會有一份,每一個消費組會有本身的offset即消費點位來標識消費到的位置。在消費點位以前的消息代表已經消費過了。固然這個offset是隊列級別的。每一個消費組都會維護訂閱的Topic下的每一個隊列的offset

來個圖看看應該就很清晰了。

基本上熟悉了消息隊列常見的術語和一些概念以後,我們再來看看消息隊列常見的核心面試點。

如何保證消息不丟失

就咱們市面上常見的消息隊列而言,只要配置得當,咱們的消息就不會丟。

先來看看這個圖,

能夠看到一共有三個階段,分別是生產消息、存儲消息和消費消息。咱們從這三個階段分別入手來看看如何確保消息不會丟失。

生產消息

生產者發送消息至Broker,須要處理Broker的響應,不管是同步仍是異步發送消息,同步和異步回調都須要作好try-catch,妥善的處理響應,若是Broker返回寫入失敗等錯誤消息,須要重試發送。當屢次發送失敗須要做報警,日誌記錄等。

這樣就能保證在生產消息階段消息不會丟失。

存儲消息

存儲消息階段須要在消息刷盤以後再給生產者響應,假設消息寫入緩存中就返回響應,那麼機器忽然斷電這消息就沒了,而生產者覺得已經發送成功了。

若是Broker是集羣部署,有多副本機制,即消息不只僅要寫入當前Broker,還須要寫入副本機中。那配置成至少寫入兩臺機子後再給生產者響應。這樣基本上就能保證存儲的可靠了。一臺掛了還有一臺還在呢(假如怕兩臺都掛了..那就再多些)。

那假如來個地震機房機子都掛了呢?emmmmmm...大公司基本上都有異地多活。

那要是這幾個地都地震了呢?emmmmmm...這時候仍是先關心關心人吧。

消費消息

這裏常常會有同窗犯錯,有些同窗當消費者拿到消息以後直接存入內存隊列中就直接返回給Broker消費成功,這是不對的。

你須要考慮拿到消息放在內存以後消費者就宕機了怎麼辦。因此咱們應該在消費者真正執行完業務邏輯以後,再發送給Broker消費成功,這纔是真正的消費了。

因此只要咱們在消息業務邏輯處理完成以後再給Broker響應,那麼消費階段消息就不會丟失。

小結一下

能夠看出,保證消息的可靠性須要三方配合

生產者須要處理好Broker的響應,出錯狀況下利用重試、報警等手段。

Broker須要控制響應的時機,單機狀況下是消息刷盤後返回響應,集羣多副本狀況下,即發送至兩個副本及以上的狀況下再返回響應。

消費者須要在執行完真正的業務邏輯以後再返回響應給Broker

可是要注意消息可靠性加強了,性能就降低了,等待消息刷盤、多副本同步後返回都會影響性能。所以仍是看業務,例如日誌的傳輸可能丟那麼一兩條關係不大,所以不必等消息刷盤再響應。

若是處理重複消息

咱們先來看看能不能避免消息的重複。

假設咱們發送消息,就管發,無論Broker的響應,那麼咱們發往Broker是不會重複的。

可是通常狀況咱們是不容許這樣的,這樣消息就徹底不可靠了,咱們的基本需求是消息至少得發到Broker上,那就得等Broker的響應,那麼就可能存在Broker已經寫入了,當時響應因爲網絡緣由生產者沒有收到,而後生產者又重發了一次,此時消息就重複了。

再看消費者消費的時候,假設咱們消費者拿到消息消費了,業務邏輯已經走完了,事務提交了,此時須要更新Consumer offset了,而後這個消費者掛了,另外一個消費者頂上,此時Consumer offset還沒更新,因而又拿到剛纔那條消息,業務又被執行了一遍。因而消息又重複了。

能夠看到正常業務而言消息重複是不可避免的,所以咱們只能從另外一個角度來解決重複消息的問題。

關鍵點就是冪等。既然咱們不能防止重複消息的產生,那麼咱們只能在業務上處理重複消息所帶來的影響。

冪等處理重複消息

冪等是數學上的概念,咱們就理解爲一樣的參數屢次調用同一個接口和調用一次產生的結果是一致的。

例如這條 SQLupdate t1 set money = 150 where id = 1 and money = 100; 執行多少遍money都是150,這就叫冪等。

所以須要改造業務處理邏輯,使得在重複消息的狀況下也不會影響最終的結果。

能夠經過上面我那條 SQL 同樣,作了個前置條件判斷,即money = 100狀況,而且直接修改,更通用的是作個version即版本號控制,對比消息中的版本號和數據庫中的版本號。

或者經過數據庫的約束例如惟一鍵,例如insert into update on duplicate key...

或者記錄關鍵的key,好比處理訂單這種,記錄訂單ID,假若有重複的消息過來,先判斷下這個ID是否已經被處理過了,若是沒處理再進行下一步。固然也能夠用全局惟一ID等等。

基本上就這麼幾個套路,真正應用到實際中仍是得看具體業務細節

如何保證消息的有序性

有序性分:全局有序和部分有序

全局有序

若是要保證消息的全局有序,首先只能由一個生產者往Topic發送消息,而且一個Topic內部只能有一個隊列(分區)。消費者也必須是單線程消費這個隊列。這樣的消息就是全局有序的!

不過通常狀況下咱們都不須要全局有序,即便是同步MySQL Binlog也只須要保證單表消息有序便可。

部分有序

所以絕大部分的有序需求是部分有序,部分有序咱們就能夠將Topic內部劃分紅咱們須要的隊列數,把消息經過特定的策略發往固定的隊列中,而後每一個隊列對應一個單線程處理的消費者。這樣即完成了部分有序的需求,又能夠經過隊列數量的併發來提升消息處理效率。

圖中我畫了多個生產者,一個生產者也能夠,只要同類消息發往指定的隊列便可。

若是處理消息堆積

消息的堆積每每是由於生產者的生產速度與消費者的消費速度不匹配。有多是由於消息消費失敗反覆重試形成的,也有可能就是消費者消費能力弱,漸漸地消息就積壓了。

所以咱們須要先定位消費慢的緣由,若是是bug則處理 bug ,若是是由於自己消費能力較弱,咱們能夠優化下消費邏輯,好比以前是一條一條消息消費處理的,此次咱們批量處理,好比數據庫的插入,一條一條插和批量插效率是不同的。

假如邏輯咱們已經都優化了,但仍是慢,那就得考慮水平擴容了,增長Topic的隊列數和消費者數量,注意隊列數必定要增長,否則新增長的消費者是沒東西消費的。一個Topic中,一個隊列只會分配給一個消費者

固然你消費者內部是單線程仍是多線程消費那看具體場景。不過要注意上面提升的消息丟失的問題,若是你是將接受到的消息寫入內存隊列以後,而後就返回響應給Broker,而後多線程向內存隊列消費消息,假設此時消費者宕機了,內存隊列裏面還未消費的消息也就丟了。

最後

上面的幾個問題都是咱們在使用消息隊列的時候常常能遇到的問題,而且也是面試關於消息隊列方面的核心考點。今天沒有深刻具體消息隊列的細節,可是套路就是這麼個套路,大方向上搞明白很關鍵。以後再接着寫有關Kafka的源碼分析文章,有興趣的小夥伴請耐心等待。

往期推薦:

圖解+代碼|常見限流算法以及限流在單機分佈式場景下的思考

Kafka請求處理全流程分析

Kafka索引設計的亮點:https://juejin.im/post/5efdeae7f265da22d017e58d

Kafka日誌段讀寫分析:https://juejin.im/post/5ef6b94ae51d4534a1236cb0


我是 yes,從一點點到億點點,咱們下篇見。


本文分享自微信公衆號 - yes的練級攻略(yes_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索