消息隊列雜談

本篇文章聊聊消息隊列相關的東西,內容侷限於咱們爲何要用消息隊列,消息隊列究竟解決了什麼問題,消息隊列的選型。web

爲了更容易的理解消息隊列,咱們首先經過一個開發場景來切入。數據庫

不使用消息隊列的場景

首先,咱們假設A同窗負責訂單系統的開發,B、C同窗負責開發積分系統、倉儲系統。咱們知道,在通常的購物電商平臺上,咱們下單完成後,積分系統會給下單的用戶增長積分,而後倉儲系統會按照下單時填寫的信息,發出用戶購買的商品。微信

那問題來了,積分系統、倉儲系統如何感知到用戶的下單操做?網絡

你可能會說,固然是訂單系統在建立完訂單以後調用積分系統、倉儲系統的接口了併發

image-20210202211720981
image-20210202211720981

OK,直接調用接口的方式在目前來看沒有什麼問題。因而B、C同窗就找到A同窗,說讓他在訂單完成後,調用一下他們的接口來通知一下積分系統和倉儲系統,來給用戶增長積分、發貨。A同窗想着,就這兩個系統,應該還好,OK我給你加了。異步

可是隨着系統的迭代,須要感知訂單操做的系統也愈來愈多,從以前的積分系統、倉儲系統2個系統,擴充到了5個。每一個系統的負責同窗都須要去找A同窗,讓他人肉的把對應系統的通知接口加上。而後就由於加了這一個接口,又須要把訂單從新發布一遍。編輯器

這對A同窗來講其實是很痛苦的一件事情,由於A同窗有本身的任務、排期,一有新系統就須要去添加通知接口,發佈服務,會打亂A的開發計劃,增長開發量。同時還須要去梳理在開發期間,新增的代碼到底能不可以上線。一旦不能上線,可是又沒有檢查到,上線就直接炸了。分佈式

image-20210202212157459
image-20210202212157459

並且,若是5個系統若是有哪一個須要額外的字段,或者是更新了接口什麼的,都須要麻煩A同窗修改。5個系統就這樣跟A系統強耦合在了一塊兒。高併發

除此以外,整個建立訂單的調用鏈由於同步調用這5個系統的通知接口而加長,這減慢了接口的響應速度,下降了用戶側的購物、下單體驗。前面的至少影響的仍是內部的員工,可是如今直接是影響到了用戶,明顯是不可取的方案。工具

image-20210203113301066
image-20210203113301066

能夠看到,整個的調用鏈路加長了,更別提,在同步調用中,若是其他的系統發生了錯誤,或者是調用其餘系統的時候出現了網絡抖動,核心的下單流程就會被阻塞住,甚至會在下單的界面提示提示用戶出錯,整個的購物體驗又被拉低了一個檔次。更況且,在實際的業務中,調用鏈比這個長的多。

可能有人會說了, 這不就是個同步調用問題嘛?訂單系統的核心邏輯,我仍是採用同步來處理,可是後續的通知我採用異步的方式,用線程池去處理,這樣調用鏈路不就恢復正常了?

image-20210203125117084
image-20210203125117084

就單純對於減小鏈路來講,的確可行。可是若是某一個流程失敗了呢?難道失敗就失敗了嗎?我下單成功了不漲積分?該給我發的貨甚至沒有發貨?這合理嗎?

unnamed
unnamed

同時,失敗了訂單系統是否是要去處理呢?不然由於其餘的系統拉垮了整個主流程,誰還來你這買東西呢?

那有什麼辦法,既可以減小調用的鏈路,又可以在發生錯誤的時候重試呢?歸根結底,核心思想就是像增長積分、返優惠券的流程不該該和主流程耦和在一塊兒,更不該該影響主流程。

試想,咱們能不能在訂單系統完成本身的核心邏輯以後,把訂單建立的消息放到一個隊列中去,而後訂單系統就返回給用戶下單成功的結果了。而後其餘的系統從這個隊列中收到了下單成功的消息,就各自的去執行各自的操做,例如增長積分、返優惠券等等操做。

後續若是有新的系統須要感知訂單建立的消息,直接去訂閱這個隊列,消費裏面的消息就行了?這雖然跟真實的消息的隊列有些出入,但其思路是完成吻合的。

爲何須要消息隊列

經過上面的例子,咱們大體就可以理解爲何要引入消息隊列了,這裏簡單總結一下。

異步

對於實時性不是很高的業務,例如給用戶發送短信、郵件通知,調用第三方的接口,均可以放到消息隊列裏去。由於相對於核心訂單流程來講,短信、郵件晚一些發送,對用戶來講影響不是很大。同時還能夠提高整個鏈路的響應時間。

削峯

假設咱們有服務A,是個無狀態的服務。經過橫向擴展,它能夠輕鬆抗住1w的併發量,可是這N個服務實例,底層訪問的都是同一個數據庫。數據庫能抗住的併發量是有限的,若是你的機器足夠好的話,可能可以抗住5000的併發,若是服務A的全部請求所有打向數據庫,會直接把數據打掛。

image-20210203143713003
image-20210203143713003

解耦

像上文舉的例子,訂單系統在建立了訂單以後須要通知其餘的全部系統,這樣一來就把訂單系統和其他的系統強耦合在了一塊兒。後續的可維護性、擴展性都大大下降了。

而經過消息隊列來關聯全部系統,能夠達到解耦的目的。

image-20210203144922191
image-20210203144922191

像上圖這種模式,若是後續再有新系統須要感知訂單建立的消息,只須要去消費「訂單系統」發送到MQ中的消息便可。一樣,訂單系統若是須要感知其他系統的某些事件,也只是從MQ中消費便可。

經過MQ,達成服務之間的鬆耦合,服務內的高內聚,提高了服務的自治性。

消息隊列選型

已知的消息隊列有Kafka、RocketMQ、RabbitMQ和ActiveMQ。可是因爲ActiveMQ如今用的公司比較少了,這裏就不作討論,咱們着重討論前三種。

Kafka

Kafka最初來自於LinkedIn,是用於作日誌收集的工具,採用Java和Scala開發。其實那個時候已經有ActiveMQ了,可是在當時ActiveMQ沒有辦法知足LinkedIn的需求,因而Kafka就應運而生。

在2010年末,Kakfa的0.7.0被開源到了Github上。到了2011年,因爲Kafka很是受關注,被歸入了Apache Incubator,全部想要成爲Apache正式項目的外部項目,都必需要通過Incubator,翻譯過來就是孵化器。旨在將一些項目孵化成徹底成熟的Apache開源項目。

你也能夠把它想象成一個學校,全部想要成爲Apache正式開源項目的外部項目都必需要進入Incubator學習,而且拿到畢業證,才能走入社會。因而在2012年,Kafka成功從Apache Incubator畢業,正式成爲Apache中的一員。

Kafka擁有很高的吞吐量,單機可以抗下十幾w的併發,並且寫入的性能也很高,可以達到毫秒級別。可是有優勢就有缺點,可以達到這麼高的併發的代價是,可能會出現消息的丟失。至於具體的丟失場景,咱們後續會討論。

因此通常Kafka都用於大數據的日誌收集,這種日誌丟個一兩條無傷大雅。

並且Kafka的功能較爲簡單,就是簡單的接收生產者的消息,消費者從Kafka消費消息。

RabbitMQ

RabbitMQ是不少公司對於ActiveMQ的替代方法,如今仍然有不少公司在使用。其優勢在於能保證消息不丟失,同Kafka,天平往數據的可靠性方向傾斜必然致使其吞吐量降低。其吞吐量只可以達到幾萬,比起Kafka的十萬吞吐來講,的確是較低的。若是遇到須要支撐特別高併發的狀況,RabbitMQ可能會沒法勝任。

可是RabbitMQ有比Kafka更多的高級特性,例如消息重試和死信隊列,並且寫入的延遲可以下降到微妙級,這也是RabbitMQ一大特色。

但RabbitMQ還有一個致命的弱點,其開發語言爲Erlang,如今國內精通Erlang的人很少,社區也不怎麼活躍。這也就致使可能公司內沒有人可以去閱讀Erlang的源碼,更別說基於其源碼進行二次開發或者排查問題了。因此就存在RabbitMQ出了問題可能公司裏沒人可以兜的住,維護成本很是的高。

之因此有中小型公司還在使用,是以爲其不會面臨高併發的場景,RabbitMQ的功能已經徹底夠用了。

RocketMQ

RocketMQ來自阿里,同Kakfa同樣也是從Apache Incubator出來的頂級項目,用Java語言進行開發,單機吞吐量和Kafka同樣,也是十w量級。

RocketMQ的前身是阿里的MetaQ項目,2012年在淘寶內部大量的使用,在阿里內部迭代到3.0版本以後,將MetaQ的核心功能抽離出來,就有了RocketMQ。RocketMQ整合了Kafka和RabbitMQ的優勢,例如較高的吞吐量和經過參數配置可以作到消息絕對不丟失。

其底層的設計參考了Kafka,具備低延遲、高性能、高可用的特色。不一樣於Kafka的單一日誌收集功能,RocketMQ被普遍運用於訂單、交易、計算、消息推送、binlog分發等場景。

之因此可以被運用到多種場景,這要歸功於RocketMQ提供的豐富的功能。例如延遲消息、事務消息、消息回溯、死信隊列等等。

  • 延遲消息 就是不會當即消費的消息,例如某個活動開始前15分鐘提醒用戶這樣的場景
  • 事務消息 其主要解決數據庫事務和MQ消息的數據一致性,例如用戶下單,先發送消息到MQ,積分增長了,可是訂單系統在發出消息以後掛了。這樣用戶並無下單成功,可是積分卻增長了,明顯是不符合預期的
  • 消息回溯 顧名思義,就是去消費某個Topic下某段時間的歷史消息
  • 死信隊列 沒有被正常消費的消息,首先會按照RocketMQ的重試機制重試,當達到了最大的重試次數以後,若是消費仍然失敗,RocketMQ不會當即丟掉這條消息,而是會把消息放入死信隊列中。放入死信隊列的消息會在3天后過時,因此須要及時的處理

消息隊列會丟消息嗎

不使用消息隊列的場景中,咱們吹了不少消息隊列的優勢,但同時也提到了消息隊列可能會丟失消息,咱們也能夠經過參數的配置來使消息絕對不丟失。

那消息是在什麼狀況下丟失的呢?消息隊列中的角色能夠分爲3類,分別是生產者、MQ和消費者。一條消息在整個的傳輸鏈路中須要通過以下的流程。

image-20210204203711613
image-20210204203711613

生產者將消息發送給MQ,MQ接收到這條消息後會將消息存儲到磁盤上,消費者來消費的時候就會把消息返給消費者。先給出結論,在這3種場景下,消息都有可能會丟失。

接下來咱們一步一步來分析一下。

生產者發送消息給MQ

生產者在發送消息的過程當中,因爲某些意外的狀況例如網絡抖動等,致使本次網絡通訊失敗,消息並無被髮送給MQ。

MQ存儲消息

當MQ接收到了來自生產者的消息以後,尚未來得及處理,MQ就忽然宕機,此時該消息也會丟失。

即便MQ開始處理消息,而且將該消息寫入了磁盤,消息仍然可能會丟失。由於現代的操做系統都會有本身的OS Cache,由於和磁盤交互是一件代價至關大的事情,因此當咱們寫入文件的時候會先將數據寫入OS Cache中,而後由OS調度,根據策略觸發真正的I/O操做,將數據刷入磁盤。

而在刷入磁盤以前,MQ若是宕機,在OS Cache中的數據就會所有丟失。

消費者消費消息

當消息順利的經歷了生產者、MQ以後,消費者拉取到了這條消息,可是當其還沒來得及處理的時候,消費者忽然宕機了,這條消息就丟了(固然你若是沒有提交Offset的話,重啓以後仍然能夠消費到這條消息)

原來咱們覺得用上了消息隊列,就萬無一失了,沒想到逐步分析下來能有這麼多坑。任何一個步驟出錯都有可能致使消息丟失。那既然這樣,上文提到的能夠經過參數配置來實現消息不會丟失是怎麼一回事呢?

這裏咱們不去聊具體的MQ是如何實現的,咱們來聊聊消息零丟失的實現思路。

消息最終一致性方案

涉及到的系統有訂單系統、MQ和積分系統,訂單系統爲生產者,積分系統爲消費者。

首先訂單系統發送一個訂單建立的消息給MQ,該消息的狀態爲Prepare狀態,狀態爲Prepare狀態的消息不會被消費者給消費到,因此能夠放心的發送。

而後訂單系統開始執行自身的核心邏輯,你可能會說,訂單系統自己的邏輯執行失敗了怎麼辦?剛剛的prepare消息不就成了髒數據?實際上在訂單系統的事務失敗以後,就會觸發回滾操做,就會向MQ發送消息,將該條狀態爲Prepare的數據給刪除。

訂單系統核心事務成功以後,就會發送消息給MQ,將狀態爲prepare的消息更新爲commit。沒錯,這就是2PC,一個保證分佈式事務數據一致性的協議。

image-20210207102443940
image-20210207102443940

眼尖的你可能發現了一個問題,我發送了prepare消息以後,還沒來得及執行本地事務,訂單系統就掛了怎麼辦?此時訂單系統即便重啓也不會向MQ發送刪除操做,這個prepare消息不就是一直存在MQ中了?

先給出結論,不會

若是訂單系統發送了prepare消息給MQ以後本身就宕機了,MQ確實會存在一條不會被commit的數據。MQ爲了解決這個問題,會定時輪詢全部prepare的消息,跟對應的系統溝通,這條prepare消息是要進行重試仍是回滾。因此prepare消息不會一直存在於MQ中。這樣一來,就保證了消息對於生產者的DB事務和MQ中消息的數據一致性

再來看一種更加極端的狀況,假設訂單系統本地事務執行成功以後,發送了commit消息到MQ,此時MQ忽然掛了。致使MQ沒有收到該commit消息,在MQ中該消息仍然處於prepare狀態,這怎麼辦?

一樣的,依賴於MQ的輪詢機制和訂單系統溝通,訂單系統會告訴MQ這個事務已經完成了,MQ就會將這條消息設置成commit,消費者就能夠消費到該消息了。

接下來的流程就是消息被消費者消費了,若是消費者消費消息的時候本地事務失敗了,則會進行重試,再次嘗試消費這條消息。

好了以上就是本篇博客的所有內容了,若是你以爲這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

歡迎微信搜索關注【SH的全棧筆記】,查看更多相關文章

相關文章
相關標籤/搜索