90%的Java程序員,都扛不住這波消息中間件的面試四連炮!
本文經(jīng)授權(quán)轉(zhuǎn)自公眾號(hào):石杉的架構(gòu)筆記
概述
大家平時(shí)也有用到一些消息中間件(MQ),但是對(duì)其理解可能僅停留在會(huì)使用API能實(shí)現(xiàn)生產(chǎn)消息、消費(fèi)消息就完事了。
對(duì)MQ更加深入的問(wèn)題,可能很多人沒(méi)怎么思考過(guò)。
比如,你跳槽面試時(shí),如果面試官看到你簡(jiǎn)歷上寫了,熟練掌握消息中間件,那么很可能給你發(fā)起如下 4 個(gè)面試連環(huán)炮!
- 為什么要使用MQ?
- 使用了MQ之后有什么優(yōu)缺點(diǎn)?
- 怎么保證MQ消息不丟失?
- 怎么保證MQ的高可用性?
本文將通過(guò)一些場(chǎng)景,配合著通俗易懂的語(yǔ)言和多張手繪彩圖,討論一下這些問(wèn)題。
為什么要使用MQ?
相信大家也聽(tīng)過(guò)這樣的一句話:好的架構(gòu)不是設(shè)計(jì)出來(lái)的,是演進(jìn)出來(lái)的。
這句話在引入MQ的場(chǎng)景同樣適用,使用MQ必定有其道理,是用來(lái)解決實(shí)際問(wèn)題的。而不是看見(jiàn)別人用了,我也用著玩兒一下。
其實(shí)使用MQ的場(chǎng)景有挺多的,但是比較核心的有3個(gè):
異步、解耦、削峰填谷
異步
我們通過(guò)實(shí)際案例說(shuō)明:假設(shè)A系統(tǒng)接收一個(gè)請(qǐng)求,需要在自己本地寫庫(kù)執(zhí)行SQL,然后需要調(diào)用BCD三個(gè)系統(tǒng)的接口。
假設(shè)自己本地寫庫(kù)要3ms,調(diào)用BCD三個(gè)系統(tǒng)分別要300ms、450ms、200ms。
那么最終請(qǐng)求總延時(shí)是3 + 300 + 450 + 200 = 953ms,接近1s,可能用戶會(huì)感覺(jué)太慢了。
此時(shí)整個(gè)系統(tǒng)大概是這樣的:
但是一旦使用了MQ之后,系統(tǒng)A只需要發(fā)送3條消息到MQ中的3個(gè)消息隊(duì)列,然后就返回給用戶了。
假設(shè)發(fā)送消息到MQ中耗時(shí)20ms,那么用戶感知到這個(gè)接口的耗時(shí)僅僅是20 + 3 = 23ms,用戶幾乎無(wú)感知,倍兒爽!
此時(shí)整個(gè)系統(tǒng)結(jié)構(gòu)大概是這樣的:
可以看到,通過(guò)MQ的異步功能,可以大大提高接口的性能。
解耦
假設(shè)A系統(tǒng)在用戶發(fā)生某個(gè)操作的時(shí)候,需要把用戶提交的數(shù)據(jù)同時(shí)推送到B、C兩個(gè)系統(tǒng)的時(shí)候。
這個(gè)時(shí)候負(fù)責(zé)A系統(tǒng)的哥們想:沒(méi)事啊,B、C兩個(gè)系統(tǒng)給我提供一個(gè)Http接口或者RPC接口,我把數(shù)據(jù)推送過(guò)去不就完事了嗎。負(fù)責(zé)A系統(tǒng)的哥們美滋滋。
如下圖所示:
一切看起來(lái)很美好,但是隨著業(yè)務(wù)快速迭代,這個(gè)時(shí)候系統(tǒng)D也想要這個(gè)數(shù)據(jù)。那既然這樣,A系統(tǒng)的開發(fā)同學(xué)就改咯,在發(fā)送數(shù)據(jù)給BC的同時(shí)加上一個(gè)D。
但是,越到后面越發(fā)現(xiàn),麻煩來(lái)了。。。
整個(gè)系統(tǒng)好像不止這個(gè)數(shù)據(jù)要發(fā)送給BCD、還有第二、第三個(gè)數(shù)據(jù)要發(fā)送給BCD。甚至有時(shí)候又加入了E、F等等系統(tǒng),他們也要這個(gè)數(shù)據(jù)。
并且有時(shí)候可能B系統(tǒng)突然又不要這個(gè)數(shù)據(jù)了,A系統(tǒng)該來(lái)改去,A系統(tǒng)的開發(fā)哥們頭皮發(fā)麻。
更復(fù)雜的場(chǎng)景是,數(shù)據(jù)通過(guò)接口傳給其他系統(tǒng)有時(shí)候還要考慮重試、超時(shí)等一些異常情況,真是頭發(fā)都白了呀。。。
來(lái)看下圖,體會(huì)一下這無(wú)助的現(xiàn)場(chǎng):
這個(gè)時(shí)候,就該我們的MQ粉墨登場(chǎng)了!
這種情況下使用MQ來(lái)解耦是在合適不過(guò)了,因?yàn)樨?fù)責(zé)A系統(tǒng)的哥們只需要把消息扔到MQ就行了,其他系統(tǒng)按需來(lái)訂閱消息就好了。
就算某個(gè)系統(tǒng)不需要這個(gè)數(shù)據(jù)了,也不會(huì)需要A系統(tǒng)改動(dòng)代碼。
看看加入MQ解耦的下圖,是不是清爽了很多!
削峰填谷
舉個(gè)例子,比如我們的訂單系統(tǒng),在下單的時(shí)候就會(huì)往數(shù)據(jù)庫(kù)寫數(shù)據(jù)。但是數(shù)據(jù)庫(kù)只能支撐每秒1000左右的并發(fā)寫入,并發(fā)量再高就容易宕機(jī)。
低峰期的時(shí)候并發(fā)也就100多個(gè),但是在高峰期時(shí)候,并發(fā)量會(huì)突然激增到5000以上,這個(gè)時(shí)候數(shù)據(jù)庫(kù)肯定死了。
如下圖,來(lái)感受一下數(shù)據(jù)庫(kù)被打死的絕望:
但是使用了MQ之后,情況就變了!
消息被MQ保存起來(lái)了,然后系統(tǒng)就可以按照自己的消費(fèi)能力來(lái)消費(fèi),比如每秒1000個(gè)數(shù)據(jù),這樣慢慢寫入數(shù)據(jù)庫(kù),這樣就不會(huì)打死數(shù)據(jù)庫(kù)了:
整個(gè)過(guò)程,如下圖所示:
至于為什么叫做削峰填谷呢?來(lái)看看這個(gè)圖:
如果沒(méi)有用MQ的情況下,并發(fā)量高峰期的時(shí)候是有一個(gè)“頂峰”的,然后高峰期過(guò)后又是一個(gè)低并發(fā)的“谷”。
但是使用了MQ之后,限制消費(fèi)消息的速度為1000,但是這樣一來(lái),高峰期產(chǎn)生的數(shù)據(jù)勢(shì)必會(huì)被積壓在MQ中,高峰就被“削”掉了。
但是因?yàn)橄⒎e壓,在高峰期過(guò)后的一段時(shí)間內(nèi),消費(fèi)消息的速度還是會(huì)維持在1000QPS,直到消費(fèi)完積壓的消息,這就叫做“填谷”
通過(guò)上面的分析,大家就可以知道為什么要使用MQ,以及使用了MQ有什么好處。知其所以然,明白了自己的系統(tǒng)為什么要使用MQ。
這樣以后別人問(wèn)你為啥要用MQ,就不會(huì)出現(xiàn) “我們組長(zhǎng)要用MQ我們就用了” 這樣尷尬的回答了。
使用了MQ之后有什么優(yōu)缺點(diǎn)?
看到這個(gè)問(wèn)題蒙圈了,用了就用了嘛!優(yōu)點(diǎn)上面已經(jīng)說(shuō)了,但是這個(gè)缺點(diǎn)是啥啊。好像沒(méi)啥缺點(diǎn)啊。
如果你這樣想,就大錯(cuò)特錯(cuò)了,在設(shè)計(jì)系統(tǒng)的過(guò)程中,除了要清楚的知道為什么要用這個(gè)東西,還要思考一下用了之后有什么壞處。這樣才能心里有底,防范于未然。
接下來(lái)我們就討論一下,用MQ會(huì)有什么缺點(diǎn)把?
系統(tǒng)可用性降低
大家想想一下,上面的說(shuō)解耦的場(chǎng)景,本來(lái)A系統(tǒng)的哥們要把系統(tǒng)關(guān)鍵數(shù)據(jù)發(fā)送給BC系統(tǒng)的,現(xiàn)在突然加入了一個(gè)MQ了,現(xiàn)在BC系統(tǒng)接收數(shù)據(jù)要通過(guò)MQ來(lái)接收。
但是大家有沒(méi)有考慮過(guò)一個(gè)問(wèn)題,萬(wàn)一MQ掛了怎么辦?這就引出一個(gè)問(wèn)題,加入了MQ之后,系統(tǒng)的可用性是不是就降低了?
因?yàn)槎嗔艘粋€(gè)風(fēng)險(xiǎn)因素:MQ可能會(huì)掛掉。只要MQ掛了,數(shù)據(jù)沒(méi)了,系統(tǒng)運(yùn)行就不對(duì)了。
系統(tǒng)復(fù)雜度提高
本來(lái)我的系統(tǒng)通過(guò)接口調(diào)用一下就能完事的,但是加入一個(gè)MQ之后,需要考慮消息重復(fù)消費(fèi)、消息丟失、甚至消息順序性的問(wèn)題
為了解決這些問(wèn)題,又需要引入很多復(fù)雜的機(jī)制,這樣一來(lái)是不是系統(tǒng)的復(fù)雜度提高了。
數(shù)據(jù)一致性問(wèn)題
本來(lái)好好的,A系統(tǒng)調(diào)用BC系統(tǒng)接口,如果BC系統(tǒng)出錯(cuò)了,會(huì)拋出異常,返回給A系統(tǒng)讓A系統(tǒng)知道,這樣的話就可以做回滾操作了
但是使用了MQ之后,A系統(tǒng)發(fā)送完消息就完事了,認(rèn)為成功了。而剛好C系統(tǒng)寫數(shù)據(jù)庫(kù)的時(shí)候失敗了,但是A認(rèn)為C已經(jīng)成功了?這樣一來(lái)數(shù)據(jù)就不一致了。
通過(guò)分析引入MQ的優(yōu)缺點(diǎn)之后,就明白了使用MQ有很多優(yōu)點(diǎn),但是會(huì)發(fā)現(xiàn)它帶來(lái)的缺點(diǎn)又會(huì)需要你做各種額外的系統(tǒng)設(shè)計(jì)來(lái)彌補(bǔ)
***你可能會(huì)發(fā)現(xiàn)整個(gè)系統(tǒng)復(fù)雜了好幾倍,所以設(shè)計(jì)系統(tǒng)的時(shí)候要基于這些考慮做出取舍,很多時(shí)候你會(huì)發(fā)現(xiàn)該用的還是要用的。。。
怎么保證MQ消息不丟失?
使用了MQ之后,還要關(guān)心消息丟失的問(wèn)題。這里我們挑RabbitMQ來(lái)說(shuō)明一下吧。
生產(chǎn)者弄丟了數(shù)據(jù)
RabbitMQ生產(chǎn)者將數(shù)據(jù)發(fā)送到rabbitmq的時(shí)候,可能數(shù)據(jù)在網(wǎng)絡(luò)傳輸中搞丟了,這個(gè)時(shí)候RabbitMQ收不到消息,消息就丟了。
RabbitMQ提供了兩種方式來(lái)解決這個(gè)問(wèn)題:
事務(wù)方式:
在生產(chǎn)者發(fā)送消息之前,通過(guò)`channel.txSelect`開啟一個(gè)事務(wù),接著發(fā)送消息
如果消息沒(méi)有成功被RabbitMQ接收到,生產(chǎn)者會(huì)收到異常,此時(shí)就可以進(jìn)行事務(wù)回滾`channel.txRollback`然后重新發(fā)送。假如RabbitMQ收到了這個(gè)消息,就可以提交事務(wù)`channel.txCommit`。
但是這樣一來(lái),生產(chǎn)者的吞吐量和性能都會(huì)降低很多,現(xiàn)在一般不這么干。
另外一種方式就是通過(guò)confirm機(jī)制:
這個(gè)confirm模式是在生產(chǎn)者哪里設(shè)置的,就是每次寫消息的時(shí)候會(huì)分配一個(gè)唯一的id,然后RabbitMQ收到之后會(huì)回傳一個(gè)ack,告訴生產(chǎn)者這個(gè)消息ok了。
如果rabbitmq沒(méi)有處理到這個(gè)消息,那么就回調(diào)一個(gè)nack的接口,這個(gè)時(shí)候生產(chǎn)者就可以重發(fā)。
事務(wù)機(jī)制和cnofirm機(jī)制***的不同在于事務(wù)機(jī)制是同步的,提交一個(gè)事務(wù)之后會(huì)阻塞在那兒
但是confirm機(jī)制是異步的,發(fā)送一個(gè)消息之后就可以發(fā)送下一個(gè)消息,然后那個(gè)消息rabbitmq接收了之后會(huì)異步回調(diào)你一個(gè)接口通知你這個(gè)消息接收到了。
所以一般在生產(chǎn)者這塊避免數(shù)據(jù)丟失,都是用confirm機(jī)制的。
Rabbitmq弄丟了數(shù)據(jù)
RabbitMQ集群也會(huì)弄丟消息,這個(gè)問(wèn)題在官方文檔的教程中也提到過(guò),就是說(shuō)在消息發(fā)送到RabbitMQ之后,默認(rèn)是沒(méi)有落地磁盤的,萬(wàn)一RabbitMQ宕機(jī)了,這個(gè)時(shí)候消息就丟失了。
所以為了解決這個(gè)問(wèn)題,RabbitMQ提供了一個(gè)持久化的機(jī)制,消息寫入之后會(huì)持久化到磁盤
這樣哪怕是宕機(jī)了,恢復(fù)之后也會(huì)自動(dòng)恢復(fù)之前存儲(chǔ)的數(shù)據(jù),這樣的機(jī)制可以確保消息不會(huì)丟失。
設(shè)置持久化有兩個(gè)步驟:
- ***個(gè)是創(chuàng)建queue的時(shí)候?qū)⑵湓O(shè)置為持久化的,這樣就可以保證rabbitmq持久化queue的元數(shù)據(jù),但是不會(huì)持久化queue里的數(shù)據(jù)
- 第二個(gè)是發(fā)送消息的時(shí)候?qū)⑾⒌膁eliveryMode設(shè)置為2,就是將消息設(shè)置為持久化的,此時(shí)rabbitmq就會(huì)將消息持久化到磁盤上去。
但是這樣一來(lái)可能會(huì)有人說(shuō):萬(wàn)一消息發(fā)送到RabbitMQ之后,還沒(méi)來(lái)得及持久化到磁盤就掛掉了,數(shù)據(jù)也丟失了,怎么辦?
對(duì)于這個(gè)問(wèn)題,其實(shí)是配合上面的confirm機(jī)制一起來(lái)保證的,就是在消息持久化到磁盤之后才會(huì)給生產(chǎn)者發(fā)送ack消息。
萬(wàn)一真的遇到了那種極端的情況,生產(chǎn)者是可以感知到的,此時(shí)生產(chǎn)者可以通過(guò)重試發(fā)送消息給別的RabbitMQ節(jié)點(diǎn)
消費(fèi)端弄丟了數(shù)據(jù)
RabbitMQ消費(fèi)端弄丟了數(shù)據(jù)的情況是這樣的:在消費(fèi)消息的時(shí)候,剛拿到消息,結(jié)果進(jìn)程掛了,這個(gè)時(shí)候RabbitMQ就會(huì)認(rèn)為你已經(jīng)消費(fèi)成功了,這條數(shù)據(jù)就丟了。
對(duì)于這個(gè)問(wèn)題,要先說(shuō)明一下RabbitMQ消費(fèi)消息的機(jī)制:在消費(fèi)者收到消息的時(shí)候,會(huì)發(fā)送一個(gè)ack給RabbitMQ,告訴RabbitMQ這條消息被消費(fèi)到了,這樣RabbitMQ就會(huì)把消息刪除。
但是默認(rèn)情況下這個(gè)發(fā)送ack的操作是自動(dòng)提交的,也就是說(shuō)消費(fèi)者一收到這個(gè)消息就會(huì)自動(dòng)返回ack給RabbitMQ,所以會(huì)出現(xiàn)丟消息的問(wèn)題。
所以針對(duì)這個(gè)問(wèn)題的解決方案就是:關(guān)閉RabbitMQ消費(fèi)者的自動(dòng)提交ack,在消費(fèi)者處理完這條消息之后再手動(dòng)提交ack。
這樣即使遇到了上面的情況,RabbitMQ也不會(huì)把這條消息刪除,會(huì)在你程序重啟之后,重新下發(fā)這條消息過(guò)來(lái)。
怎么保證MQ的高可用性性?
使用了MQ之后,我們肯定是希望MQ有高可用特性,因?yàn)椴豢赡芙邮軝C(jī)器宕機(jī)了,就無(wú)法收發(fā)消息的情況。
這一塊我們也是基于RabbitMQ這種經(jīng)典的MQ來(lái)說(shuō)明一下:
RabbitMQ是比較有代表性的,因?yàn)槭腔谥鲝淖龈呖捎眯缘?,我們就以他為例子講解***種MQ的高可用性怎么實(shí)現(xiàn)。
rabbitmq有三種模式:?jiǎn)螜C(jī)模式,普通集群模式,鏡像集群模式
單機(jī)模式
單機(jī)模式就是demo級(jí)別的,就是說(shuō)只有一臺(tái)機(jī)器部署了一個(gè)RabbitMQ程序。
這個(gè)會(huì)存在單點(diǎn)問(wèn)題,宕機(jī)就玩完了,沒(méi)什么高可用性可言。一般就是你本地啟動(dòng)了玩玩兒的,沒(méi)人生產(chǎn)用單機(jī)模式。
普通集群模式
這個(gè)模式的意思就是在多臺(tái)機(jī)器上啟動(dòng)多個(gè)rabbitmq實(shí)例。類似的master-slave模式一樣。
但是創(chuàng)建的queue,只會(huì)放在一個(gè)master rabbtimq實(shí)例上,其他實(shí)例都同步那個(gè)接收消息的RabbitMQ元數(shù)據(jù)。
在消費(fèi)消息的時(shí)候,如果你連接到的RabbitMQ實(shí)例不是存放Queue數(shù)據(jù)的實(shí)例,這個(gè)時(shí)候RabbitMQ就會(huì)從存放Queue數(shù)據(jù)的實(shí)例上拉去數(shù)據(jù),然后返回給客戶端。
總的來(lái)說(shuō),這種方式有點(diǎn)麻煩,沒(méi)有做到真正的分布式,每次消費(fèi)者連接一個(gè)實(shí)例后拉取數(shù)據(jù),如果連接到不是存放queue數(shù)據(jù)的實(shí)例,這個(gè)時(shí)候會(huì)造成額外的性能開銷。如果從放Queue的實(shí)例拉取,會(huì)導(dǎo)致單實(shí)例性能瓶頸。
如果放queue的實(shí)例宕機(jī)了,會(huì)導(dǎo)致其他實(shí)例無(wú)法拉取數(shù)據(jù),這個(gè)集群都無(wú)法消費(fèi)消息了,沒(méi)有做到真正的高可用。
所以這個(gè)事兒就比較尷尬了,這就沒(méi)有什么所謂的高可用性可言了,這方案主要是提高吞吐量的,就是說(shuō)讓集群中多個(gè)節(jié)點(diǎn)來(lái)服務(wù)某個(gè)queue的讀寫操作。
鏡像集群模式
鏡像集群模式才是真正的rabbitmq的高可用模式,跟普通集群模式不一樣的是:創(chuàng)建的queue無(wú)論元數(shù)據(jù)還是queue里的消息都會(huì)存在于多個(gè)實(shí)例上,
每次寫消息到queue的時(shí)候,都會(huì)自動(dòng)把消息到多個(gè)實(shí)例的queue里進(jìn)行消息同步。
這樣的話任何一個(gè)機(jī)器宕機(jī)了別的實(shí)例都可以用提供服務(wù),這樣就做到了真正的高可用了。
但是也存在著不好之處:
- 性能開銷過(guò)高,消息需要同步所有機(jī)器,會(huì)導(dǎo)致網(wǎng)絡(luò)帶寬壓力和消耗很重
- 擴(kuò)展性低:無(wú)法解決某個(gè)queue數(shù)據(jù)量特別大的情況,導(dǎo)致queue無(wú)法線性拓展。
就算加了機(jī)器,那個(gè)機(jī)器也會(huì)包含queue的所有數(shù)據(jù),queue的數(shù)據(jù)沒(méi)有做到分布式存儲(chǔ)。
對(duì)于RabbitMQ的高可用一般的做法都是開啟鏡像集群模式,這樣起碼來(lái)說(shuō)做到了高可用,一個(gè)節(jié)點(diǎn)宕機(jī)了,其他節(jié)點(diǎn)可以繼續(xù)提供服務(wù)。
總結(jié)
通過(guò)本篇文章,分析了對(duì)于MQ的一些常規(guī)問(wèn)題:
- 為什么使用MQ?
- 使用MQ有什么優(yōu)缺點(diǎn)
- 如何保證消息不丟失?
- 如何保證MQ高可用性?
但是,這些問(wèn)題僅僅是使用MQ的其中一部分需要考慮的問(wèn)題,事實(shí)上,還有其他更加復(fù)雜的問(wèn)題需要我們?nèi)ソ鉀Q,
比如:如何保證消息的順序性?消息隊(duì)列如何選型?消息積壓?jiǎn)栴}如何解決?
本文僅僅是針對(duì)RabbitMQ的場(chǎng)景舉例子。還有其他比較的消息隊(duì)列,比如RocketMQ、Kafka。
不同的MQ在面臨上述問(wèn)題的時(shí)候,要根據(jù)他們的原理機(jī)制來(lái)做對(duì)應(yīng)的處理,這些都是本文沒(méi)有顧及的內(nèi)容,將在后面的文章中討論。敬請(qǐng)關(guān)注。