90%的Java程序員,都扛不住這波消息中間件的面試四連炮!

本文來自公衆號讀者jianfeng的投稿

---------------------------------------面試

概述

你們平時也有用到一些消息中間件(MQ),可是對其理解可能僅停留在會使用API能實現生產消息、消費消息就完事了。數據庫

對MQ更加深刻的問題,可能不少人沒怎麼思考過。微信

好比,你跳槽面試時,若是面試官看到你簡歷上寫了,熟練掌握消息中間件,那麼極可能給你發起以下 4 個面試連環炮!網絡

  • 爲何要使用MQ?架構

  • 使用了MQ以後有什麼優缺點?併發

  • 怎麼保證MQ消息不丟失?異步

  • 怎麼保證MQ的高可用性?分佈式


本文將經過一些場景,配合着通俗易懂的語言和多張手繪彩圖,討論一下這些問題。性能


爲何要使用MQ?

相信你們也聽過這樣的一句話:好的架構不是設計出來的,是演進出來的設計

這句話在引入MQ的場景一樣適用,使用MQ一定有其道理,是用來解決實際問題的。而不是看見別人用了,我也用着玩兒一下。

其實使用MQ的場景有挺多的,可是比較核心的有3個:

異步、解耦、削峯填谷


異步

咱們經過實際案例說明:假設A系統接收一個請求,須要在本身本地寫庫執行SQL,而後須要調用BCD三個系統的接口。

假設本身本地寫庫要3ms,調用BCD三個系統分別要300ms、450ms、200ms。

那麼最終請求總延時是3 + 300 + 450 + 200 = 953ms,接近1s,可能用戶會感受太慢了。


此時整個系統大概是這樣的:

可是一旦使用了MQ以後,系統A只須要發送3條消息到MQ中的3個消息隊列,而後就返回給用戶了。

假設發送消息到MQ中耗時20ms,那麼用戶感知到這個接口的耗時僅僅是20 + 3 = 23ms,用戶幾乎無感知,倍兒爽!

此時整個系統結構大概是這樣的:

能夠看到,經過MQ的異步功能,能夠大大提升接口的性能。


解耦

假設A系統在用戶發生某個操做的時候,須要把用戶提交的數據同時推送到B、C兩個系統的時候。

這個時候負責A系統的哥們想:沒事啊,B、C兩個系統給我提供一個Http接口或者RPC接口,我把數據推送過去不就完事了嗎。負責A系統的哥們美滋滋。

以下圖所示:

一切看起來很美好,可是隨着業務快速迭代,這個時候系統D也想要這個數據。那既然這樣,A系統的開發同窗就改咯,在發送數據給BC的同時加上一個D。

可是,越到後面愈加現,麻煩來了。。。

整個系統好像不止這個數據要發送給BCD、還有第2、第三個數據要發送給BCD。甚至有時候又加入了E、F等等系統,他們也要這個數據。

而且有時候可能B系統忽然又不要這個數據了,A系統該來改去,A系統的開發哥們頭皮發麻。

更復雜的場景是,數據經過接口傳給其餘系統有時候還要考慮重試、超時等一些異常狀況,真是頭髮都白了呀。。。


來看下圖,體會一下這無助的現場:

這個時候,就該咱們的MQ粉墨登場了!

這種狀況下使用MQ來解耦是在合適不過了,由於負責A系統的哥們只須要把消息扔到MQ就好了,其餘系統按需來訂閱消息就行了。

就算某個系統不須要這個數據了,也不會須要A系統改動代碼。

看看加入MQ解耦的下圖,是否是清爽了不少!


削峯填谷

舉個例子,好比咱們的訂單系統,在下單的時候就會往數據庫寫數據。可是數據庫只能支撐每秒1000左右的併發寫入,併發量再高就容易宕機。

低峯期的時候併發也就100多個,可是在高峯期時候,併發量會忽然激增到5000以上,這個時候數據庫確定死了。

以下圖,來感覺一下數據庫被打死的絕望:

可是使用了MQ以後,狀況就變了!

消息被MQ保存起來了,而後系統就能夠按照本身的消費能力來消費,好比每秒1000個數據,這樣慢慢寫入數據庫,這樣就不會打死數據庫了:

整個過程,以下圖所示:


至於爲何叫作削峯填谷呢?來看看這個圖:

若是沒有用MQ的狀況下,併發量高峯期的時候是有一個「頂峯」的,而後高峯期事後又是一個低併發的「」。

可是使用了MQ以後,限制消費消息的速度爲1000,可是這樣一來,高峯期產生的數據勢必會被積壓在MQ中,高峯就被「削」掉了。

可是由於消息積壓,在高峯期事後的一段時間內,消費消息的速度仍是會維持在1000QPS,直到消費完積壓的消息,這就叫作「填谷」

經過上面的分析,你們就能夠知道爲何要使用MQ,以及使用了MQ有什麼好處。知其因此然,明白了本身的系統爲何要使用MQ。

這樣之後別人問你爲啥要用MQ,就不會出現 「咱們組長要用MQ咱們就用了」 這樣尷尬的回答了。


使用了MQ以後有什麼優缺點?

看到這個問題蒙圈了,用了就用了嘛!優勢上面已經說了,可是這個缺點是啥啊。好像沒啥缺點啊。

若是你這樣想,就大錯特錯了,在設計系統的過程當中,除了要清楚的知道爲何要用這個東西,還要思考一下用了以後有什麼壞處。這樣才能內心有底,防範於未然。

接下來咱們就討論一下,用MQ會有什麼缺點把?


系統可用性下降

你們想一想一下,上面的說解耦的場景,原本A系統的哥們要把系統關鍵數據發送給BC系統的,如今忽然加入了一個MQ了,如今BC系統接收數據要經過MQ來接收。

可是你們有沒有考慮過一個問題,萬一MQ掛了怎麼辦?這就引出一個問題,加入了MQ以後,系統的可用性是否是就下降了?

由於多了一個風險因素:MQ可能會掛掉。只要MQ掛了,數據沒了,系統運行就不對了。


系統複雜度提升

原本個人系統經過接口調用一下就能完事的,可是加入一個MQ以後,須要考慮消息重複消費、消息丟失、甚至消息順序性的問題

爲了解決這些問題,又須要引入不少複雜的機制,這樣一來是否是系統的複雜度提升了。


數據一致性問題

原本好好的,A系統調用BC系統接口,若是BC系統出錯了,會拋出異常,返回給A系統讓A系統知道,這樣的話就能夠作回滾操做了

可是使用了MQ以後,A系統發送完消息就完事了,認爲成功了。而恰好C系統寫數據庫的時候失敗了,可是A認爲C已經成功了?這樣一來數據就不一致了。

經過分析引入MQ的優缺點以後,就明白了使用MQ有不少優勢,可是會發現它帶來的缺點又會須要你作各類額外的系統設計來彌補

最後你可能會發現整個系統複雜了好幾倍,因此設計系統的時候要基於這些考慮作出取捨,不少時候你會發現該用的仍是要用的。。。

怎麼保證MQ消息不丟失?

使用了MQ以後,還要關心消息丟失的問題。這裏咱們挑RabbitMQ來講明一下吧。

生產者弄丟了數據

RabbitMQ生產者將數據發送到rabbitmq的時候,可能數據在網絡傳輸中搞丟了,這個時候RabbitMQ收不到消息,消息就丟了。

RabbitMQ提供了兩種方式來解決這個問題:

事務方式:

在生產者發送消息以前,經過`channel.txSelect`開啓一個事務,接着發送消息

若是消息沒有成功被RabbitMQ接收到,生產者會收到異常,此時就能夠進行事務回滾`channel.txRollback`而後從新發送。假如RabbitMQ收到了這個消息,就能夠提交事務`channel.txCommit`。

可是這樣一來,生產者的吞吐量和性能都會下降不少,如今通常不這麼幹。

另一種方式就是經過confirm機制:

這個confirm模式是在生產者哪裏設置的,就是每次寫消息的時候會分配一個惟一的id,而後RabbitMQ收到以後會回傳一個ack,告訴生產者這個消息ok了。

若是rabbitmq沒有處理到這個消息,那麼就回調一個nack的接口,這個時候生產者就能夠重發。

事務機制和cnofirm機制最大的不一樣在於事務機制是同步的,提交一個事務以後會阻塞在那兒

可是confirm機制是異步的,發送一個消息以後就能夠發送下一個消息,而後那個消息rabbitmq接收了以後會異步回調你一個接口通知你這個消息接收到了。

因此通常在生產者這塊避免數據丟失,都是用confirm機制的

Rabbitmq弄丟了數據

RabbitMQ集羣也會弄丟消息,這個問題在官方文檔的教程中也提到過,就是說在消息發送到RabbitMQ以後,默認是沒有落地磁盤的,萬一RabbitMQ宕機了,這個時候消息就丟失了。

因此爲了解決這個問題,RabbitMQ提供了一個持久化的機制,消息寫入以後會持久化到磁盤

這樣哪怕是宕機了,恢復以後也會自動恢復以前存儲的數據,這樣的機制能夠確保消息不會丟失。

設置持久化有兩個步驟:

  • 第一個是建立queue的時候將其設置爲持久化的,這樣就能夠保證rabbitmq持久化queue的元數據,可是不會持久化queue裏的數據

  • 第二個是發送消息的時候將消息的deliveryMode設置爲2,就是將消息設置爲持久化的,此時rabbitmq就會將消息持久化到磁盤上去。


可是這樣一來可能會有人說:萬一消息發送到RabbitMQ以後,還沒來得及持久化到磁盤就掛掉了,數據也丟失了,怎麼辦?

對於這個問題,實際上是配合上面的confirm機制一塊兒來保證的,就是在消息持久化到磁盤以後纔會給生產者發送ack消息。

萬一真的遇到了那種極端的狀況,生產者是能夠感知到的,此時生產者能夠經過重試發送消息給別的RabbitMQ節點


消費端弄丟了數據

RabbitMQ消費端弄丟了數據的狀況是這樣的:在消費消息的時候,剛拿到消息,結果進程掛了,這個時候RabbitMQ就會認爲你已經消費成功了,這條數據就丟了。

對於這個問題,要先說明一下RabbitMQ消費消息的機制:在消費者收到消息的時候,會發送一個ack給RabbitMQ,告訴RabbitMQ這條消息被消費到了,這樣RabbitMQ就會把消息刪除。

可是默認狀況下這個發送ack的操做是自動提交的,也就是說消費者一收到這個消息就會自動返回ack給RabbitMQ,因此會出現丟消息的問題。

因此針對這個問題的解決方案就是:關閉RabbitMQ消費者的自動提交ack,在消費者處理完這條消息以後再手動提交ack。

這樣即便遇到了上面的狀況,RabbitMQ也不會把這條消息刪除,會在你程序重啓以後,從新下發這條消息過來。


怎麼保證MQ的高可用性性?

使用了MQ以後,咱們確定是但願MQ有高可用特性,由於不可能接受機器宕機了,就沒法收發消息的狀況。

這一塊咱們也是基於RabbitMQ這種經典的MQ來講明一下:

RabbitMQ是比較有表明性的,由於是基於主從作高可用性的,咱們就以他爲例子講解第一種MQ的高可用性怎麼實現。

rabbitmq有三種模式:單機模式,普通集羣模式,鏡像集羣模式

單機模式

單機模式就是demo級別的,就是說只有一臺機器部署了一個RabbitMQ程序。

這個會存在單點問題,宕機就玩完了,沒什麼高可用性可言。通常就是你本地啓動了玩玩兒的,沒人生產用單機模式。

普通集羣模式

這個模式的意思就是在多臺機器上啓動多個rabbitmq實例。相似的master-slave模式同樣。

可是建立的queue,只會放在一個master rabbtimq實例上,其餘實例都同步那個接收消息的RabbitMQ元數據。

在消費消息的時候,若是你鏈接到的RabbitMQ實例不是存放Queue數據的實例,這個時候RabbitMQ就會從存放Queue數據的實例上拉去數據,而後返回給客戶端。

總的來講,這種方式有點麻煩,沒有作到真正的分佈式,每次消費者鏈接一個實例後拉取數據,若是鏈接到不是存放queue數據的實例,這個時候會形成額外的性能開銷。若是從放Queue的實例拉取,會致使單實例性能瓶頸。

若是放queue的實例宕機了,會致使其餘實例沒法拉取數據,這個集羣都沒法消費消息了,沒有作到真正的高可用。

因此這個事兒就比較尷尬了,這就沒有什麼所謂的高可用性可言了,這方案主要是提升吞吐量的,就是說讓集羣中多個節點來服務某個queue的讀寫操做。


鏡像集羣模式

鏡像集羣模式纔是真正的rabbitmq的高可用模式,跟普通集羣模式不同的是:建立的queue不管元數據仍是queue裏的消息都會存在於多個實例上,

每次寫消息到queue的時候,都會自動把消息到多個實例的queue裏進行消息同步。

這樣的話任何一個機器宕機了別的實例均可以用提供服務,這樣就作到了真正的高可用了。

可是也存在着很差之處:

  • 性能開銷太高,消息須要同步全部機器,會致使網絡帶寬壓力和消耗很重

  • 擴展性低:沒法解決某個queue數據量特別大的狀況,致使queue沒法線性拓展。

    就算加了機器,那個機器也會包含queue的全部數據,queue的數據沒有作到分佈式存儲。

對於RabbitMQ的高可用通常的作法都是開啓鏡像集羣模式,這樣起碼來講作到了高可用,一個節點宕機了,其餘節點能夠繼續提供服務。


總結

經過本篇文章,分析了對於MQ的一些常規問題:

  • 爲何使用MQ?

  • 使用MQ有什麼優缺點

  • 如何保證消息不丟失?

  • 如何保證MQ高可用性?


可是,這些問題僅僅是使用MQ的其中一部分須要考慮的問題,事實上,還有其餘更加複雜的問題須要咱們去解決,

好比:如何保證消息的順序性?消息隊列如何選型?消息積壓問題如何解決?

本文僅僅是針對RabbitMQ的場景舉例子。還有其餘比較的消息隊列,好比RocketMQ、Kafka

不一樣的MQ在面臨上述問題的時候,要根據他們的原理機制來作對應的處理,這些都是本文沒有顧及的內容,將在後面的文章中討論。敬請關注。

End

推薦一個專欄:

《從零開始帶你成爲JVM實戰高手》

做者是我多年好友,之前團隊的左膀右臂

一塊兒經歷過各類大型複雜系統上線的血雨腥風

現任阿里資深技術專家,對JVM有豐富的生產實踐經驗

專欄目錄參見文末,能夠掃下方海報進行試讀

經過上面海報購買,再返你24元

領取方式:加微信號:Giotto1245,暗號:返現

相關文章
相關標籤/搜索