為什么使用消息隊(duì)列
解耦
看這么個(gè)場(chǎng)景。A 系統(tǒng)發(fā)送數(shù)據(jù)到 BCD 三個(gè)系統(tǒng),通過(guò)接口調(diào)用發(fā)送。如果 E 系統(tǒng)也要這個(gè)數(shù)據(jù)呢?那如果 C 系統(tǒng)現(xiàn)在不需要了呢?A 系統(tǒng)負(fù)責(zé)人幾乎崩潰......

在這個(gè)場(chǎng)景中,A 系統(tǒng)跟其它各種亂七八糟的系統(tǒng)嚴(yán)重耦合,A 系統(tǒng)產(chǎn)生一條比較關(guān)鍵的數(shù)據(jù),很多系統(tǒng)都需要 A 系統(tǒng)將這個(gè)數(shù)據(jù)發(fā)送過(guò)來(lái)。A 系統(tǒng)要時(shí)時(shí)刻刻考慮 BCDE 四個(gè)系統(tǒng)如果掛了該咋辦?要不要重發(fā),要不要把消息存起來(lái)?頭發(fā)都白了??!
如果使用 MQ,A 系統(tǒng)產(chǎn)生一條數(shù)據(jù),發(fā)送到 MQ 里面去,哪個(gè)系統(tǒng)需要數(shù)據(jù)自己去 MQ 里面消費(fèi)。如果新系統(tǒng)需要數(shù)據(jù),直接從 MQ 里消費(fèi)即可;如果某個(gè)系統(tǒng)不需要這條數(shù)據(jù)了,就取消對(duì) MQ 消息的消費(fèi)即可。這樣下來(lái),A 系統(tǒng)壓根兒不需要去考慮要給誰(shuí)發(fā)送數(shù)據(jù),不需要維護(hù)這個(gè)代碼,也不需要考慮人家是否調(diào)用成功、失敗超時(shí)等情況。

總結(jié):通過(guò)一個(gè) MQ,Pub/Sub 發(fā)布訂閱消息這么一個(gè)模型,A 系統(tǒng)就跟其它系統(tǒng)徹底解耦了。你需要去考慮一下你負(fù)責(zé)的系統(tǒng)中是否有類似的場(chǎng)景,就是一個(gè)系統(tǒng)或者一個(gè)模塊,調(diào)用了多個(gè)系統(tǒng)或者模塊,互相之間的調(diào)用很復(fù)雜,維護(hù)起來(lái)很麻煩。但是其實(shí)這個(gè)調(diào)用是不需要直接同步調(diào)用接口的,如果用 MQ 給它異步化解耦,也是可以的,你就需要去考慮在你的項(xiàng)目里,是不是可以運(yùn)用這個(gè) MQ 去進(jìn)行系統(tǒng)的解耦。在簡(jiǎn)歷中體現(xiàn)出來(lái)這塊東西,用 MQ 作解耦。
異步
再來(lái)看一個(gè)場(chǎng)景,A 系統(tǒng)接收一個(gè)請(qǐng)求,需要在自己本地寫庫(kù),還需要在 BCD 三個(gè)系統(tǒng)寫庫(kù),自己本地寫庫(kù)要 3ms,BCD 三個(gè)系統(tǒng)分別寫庫(kù)要 300ms、450ms、200ms。最終請(qǐng)求總延時(shí)是 3 + 300 + 450 + 200 = 953ms,接近 1s,用戶感覺(jué)搞個(gè)什么東西,慢死了慢死了。用戶通過(guò)瀏覽器發(fā)起請(qǐng)求,等待個(gè) 1s,這幾乎是不可接受的。

一般互聯(lián)網(wǎng)類的企業(yè),對(duì)于用戶直接的操作,一般要求是每個(gè)請(qǐng)求都必須在 200 ms 以內(nèi)完成,對(duì)用戶幾乎是無(wú)感知的。
如果使用 MQ,那么 A 系統(tǒng)連續(xù)發(fā)送 3 條消息到 MQ 隊(duì)列中,假如耗時(shí) 5ms,A 系統(tǒng)從接受一個(gè)請(qǐng)求到返回響應(yīng)給用戶,總時(shí)長(zhǎng)是 3 + 5 = 8ms,對(duì)于用戶而言,其實(shí)感覺(jué)上就是點(diǎn)個(gè)按鈕,8ms 以后就直接返回了,爽!網(wǎng)站做得真好,真快!

削峰
每天 0:00 到 12:00,A 系統(tǒng)風(fēng)平浪靜,每秒并發(fā)請(qǐng)求數(shù)量就 50 個(gè)。結(jié)果每次一到 12:00 ~ 13:00 ,每秒并發(fā)請(qǐng)求數(shù)量突然會(huì)暴增到 5k+ 條。但是系統(tǒng)是直接基于 MySQL 的,大量的請(qǐng)求涌入 MySQL,每秒鐘對(duì) MySQL 執(zhí)行約 5k 條 SQL。
一般的 MySQL,扛到每秒 2k 個(gè)請(qǐng)求就差不多了,如果每秒請(qǐng)求到 5k 的話,可能就直接把 MySQL 給打死了,導(dǎo)致系統(tǒng)崩潰,用戶也就沒(méi)法再使用系統(tǒng)了。
但是高峰期一過(guò),到了下午的時(shí)候,就成了低峰期,可能也就 1w 的用戶同時(shí)在網(wǎng)站上操作,每秒中的請(qǐng)求數(shù)量可能也就 50 個(gè)請(qǐng)求,對(duì)整個(gè)系統(tǒng)幾乎沒(méi)有任何的壓力。

如果使用 MQ,每秒 5k 個(gè)請(qǐng)求寫入 MQ,A 系統(tǒng)每秒鐘最多處理 2k 個(gè)請(qǐng)求,因?yàn)?MySQL 每秒鐘最多處理 2k 個(gè)。A 系統(tǒng)從 MQ 中慢慢拉取請(qǐng)求,每秒鐘就拉取 2k 個(gè)請(qǐng)求,不要超過(guò)自己每秒能處理的最大請(qǐng)求數(shù)量就 ok,這樣下來(lái),哪怕是高峰期的時(shí)候,A 系統(tǒng)也絕對(duì)不會(huì)掛掉。而 MQ 每秒鐘 5k 個(gè)請(qǐng)求進(jìn)來(lái),就 2k 個(gè)請(qǐng)求出去,結(jié)果就導(dǎo)致在中午高峰期(1 個(gè)小時(shí)),可能有幾十萬(wàn)甚至幾百萬(wàn)的請(qǐng)求積壓在 MQ 中。

這個(gè)短暫的高峰期積壓是 ok 的,因?yàn)楦叻迤谶^(guò)了之后,每秒鐘就 50 個(gè)請(qǐng)求進(jìn) MQ,但是 A 系統(tǒng)依然會(huì)按照每秒 2k 個(gè)請(qǐng)求的速度在處理。所以說(shuō),只要高峰期一過(guò),A 系統(tǒng)就會(huì)快速將積壓的消息給解決掉。
消息隊(duì)列的缺點(diǎn)
系統(tǒng)可用性降低
系統(tǒng)引入的外部依賴越多,越容易掛掉。本來(lái)你就是 A 系統(tǒng)調(diào)用 BCD 三個(gè)系統(tǒng)的接口就好了,ABCD 四個(gè)系統(tǒng)還好好的,沒(méi)啥問(wèn)題,你偏加個(gè) MQ 進(jìn)來(lái),萬(wàn)一 MQ 掛了咋整?MQ 一掛,整套系統(tǒng)崩潰,你不就完了?
系統(tǒng)復(fù)雜度提高
硬生生加個(gè) MQ 進(jìn)來(lái),你怎么保證消息沒(méi)有重復(fù)消費(fèi)?怎么處理消息丟失的情況?怎么保證消息傳遞的順序性?頭大頭大,問(wèn)題一大堆,痛苦不已。
一致性問(wèn)題
A 系統(tǒng)處理完了直接返回成功了,人都以為你這個(gè)請(qǐng)求就成功了;但是問(wèn)題是,要是 BCD 三個(gè)系統(tǒng)那里,BD 兩個(gè)系統(tǒng)寫庫(kù)成功了,結(jié)果 C 系統(tǒng)寫庫(kù)失敗了,咋整?你這數(shù)據(jù)就不一致了。
ActiveMQ,RabbitMQ,RocketMQ,Kafka有什么異同
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
|---|---|---|---|---|
| 開(kāi)發(fā)語(yǔ)言 | java | erlang | java | scala |
| 單機(jī)吞吐量 | 萬(wàn)級(jí),比 RocketMQ、Kafka 低一個(gè)數(shù)量級(jí) | 同 ActiveMQ | 10 萬(wàn)級(jí),支撐高吞吐 | 10 萬(wàn)級(jí),高吞吐,一般配合大數(shù)據(jù)類的系統(tǒng)來(lái)進(jìn)行實(shí)時(shí)數(shù)據(jù)計(jì)算、日志采集等場(chǎng)景 |
| topic 數(shù)量對(duì)吞吐量的影響 | topic 可以達(dá)到幾百/幾千的級(jí)別,吞吐量會(huì)有較小幅度的下降,這是 RocketMQ 的一大優(yōu)勢(shì),在同等機(jī)器下,可以支撐大量的 topic | topic 從幾十到幾百個(gè)時(shí)候,吞吐量會(huì)大幅度下降,在同等機(jī)器下,Kafka 盡量保證 topic 數(shù)量不要過(guò)多,如果要支撐大規(guī)模的 topic,需要增加更多的機(jī)器資源 | ||
| 時(shí)效性 | ms 級(jí) | 微秒級(jí),這是 RabbitMQ 的一大特點(diǎn),延遲最低 | ms 級(jí) | 延遲在 ms 級(jí)以內(nèi) |
| 可用性 | 高,基于主從架構(gòu)實(shí)現(xiàn)高可用 | 同 ActiveMQ | 非常高,分布式架構(gòu) | 非常高,分布式,一個(gè)數(shù)據(jù)多個(gè)副本,少數(shù)機(jī)器宕機(jī),不會(huì)丟失數(shù)據(jù),不會(huì)導(dǎo)致不可用 |
| 消息可靠性 | 有較低的概率丟失數(shù)據(jù) | 基本不丟 | 經(jīng)過(guò)參數(shù)優(yōu)化配置,可以做到 0 丟失 | 同 RocketMQ |
| 功能支持 | MQ 領(lǐng)域的功能極其完備 | 基于 erlang 開(kāi)發(fā),并發(fā)能力很強(qiáng),性能極好,延時(shí)很低 | MQ 功能較為完善,還是分布式的,擴(kuò)展性好 | 功能較為簡(jiǎn)單,主要支持簡(jiǎn)單的 MQ 功能,在大數(shù)據(jù)領(lǐng)域的實(shí)時(shí)計(jì)算以及日志采集被大規(guī)模使用 |
| 社區(qū)活躍度 | 低 | 很高 | 一般 | 很高 |
- 中小型公司,技術(shù)實(shí)力較為一般,技術(shù)挑戰(zhàn)不是特別高,用 RabbitMQ 是不錯(cuò)的選擇;
- 大型公司,基礎(chǔ)架構(gòu)研發(fā)實(shí)力較強(qiáng),用 RocketMQ 是很好的選擇。
- 大數(shù)據(jù)領(lǐng)域的實(shí)時(shí)計(jì)算、日志采集等場(chǎng)景,用 Kafka 是業(yè)內(nèi)標(biāo)準(zhǔn)的,幾乎是全世界這個(gè)領(lǐng)域的事實(shí)性規(guī)范。
如何保證消息隊(duì)列的高可用?
RabbitMQ 的高可用性
RabbitMQ 是比較有代表性的,因?yàn)槭腔谥鲝模ǚ欠植际剑┳龈呖捎眯缘?,我們就?RabbitMQ 為例子講解第一種 MQ 的高可用性怎么實(shí)現(xiàn)。
RabbitMQ 有三種模式:?jiǎn)螜C(jī)模式、普通集群模式、鏡像集群模式。
單機(jī)模式
單機(jī)模式,就是 Demo 級(jí)別的,一般就是你本地啟動(dòng)了玩玩兒的??,沒(méi)人生產(chǎn)用單機(jī)模式。
普通集群模式(無(wú)高可用性)
普通集群模式,意思就是在多臺(tái)機(jī)器上啟動(dòng)多個(gè) RabbitMQ 實(shí)例,每個(gè)機(jī)器啟動(dòng)一個(gè)。你創(chuàng)建的 queue,只會(huì)放在一個(gè) RabbitMQ 實(shí)例上,但是每個(gè)實(shí)例都同步 queue 的元數(shù)據(jù)(元數(shù)據(jù)可以認(rèn)為是 queue 的一些配置信息,通過(guò)元數(shù)據(jù),可以找到 queue 所在實(shí)例)。你消費(fèi)的時(shí)候,實(shí)際上如果連接到了另外一個(gè)實(shí)例,那么那個(gè)實(shí)例會(huì)從 queue 所在實(shí)例上拉取數(shù)據(jù)過(guò)來(lái)。

這種方式確實(shí)很麻煩,也不怎么好,沒(méi)做到所謂的分布式,就是個(gè)普通集群。因?yàn)檫@導(dǎo)致你要么消費(fèi)者每次隨機(jī)連接一個(gè)實(shí)例然后拉取數(shù)據(jù),要么固定連接那個(gè) queue 所在實(shí)例消費(fèi)數(shù)據(jù),前者有數(shù)據(jù)拉取的開(kāi)銷,后者導(dǎo)致單實(shí)例性能瓶頸。
而且如果那個(gè)放 queue 的實(shí)例宕機(jī)了,會(huì)導(dǎo)致接下來(lái)其他實(shí)例就無(wú)法從那個(gè)實(shí)例拉取,如果你開(kāi)啟了消息持久化,讓 RabbitMQ 落地存儲(chǔ)消息的話,消息不一定會(huì)丟,得等這個(gè)實(shí)例恢復(fù)了,然后才可以繼續(xù)從這個(gè) queue 拉取數(shù)據(jù)。
所以這個(gè)事兒就比較尷尬了,這就沒(méi)有什么所謂的高可用性,這方案主要是提高吞吐量的,就是說(shuō)讓集群中多個(gè)節(jié)點(diǎn)來(lái)服務(wù)某個(gè) queue 的讀寫操作。
鏡像集群模式(高可用性)
這種模式,才是所謂的 RabbitMQ 的高可用模式。跟普通集群模式不一樣的是,在鏡像集群模式下,你創(chuàng)建的 queue,無(wú)論元數(shù)據(jù)還是 queue 里的消息都會(huì)存在于多個(gè)實(shí)例上,就是說(shuō),每個(gè) RabbitMQ 節(jié)點(diǎn)都有這個(gè) queue 的一個(gè)完整鏡像,包含 queue 的全部數(shù)據(jù)的意思。然后每次你寫消息到 queue 的時(shí)候,都會(huì)自動(dòng)把消息同步到多個(gè)實(shí)例的 queue 上。

那么如何開(kāi)啟這個(gè)鏡像集群模式呢?其實(shí)很簡(jiǎn)單,RabbitMQ 有很好的管理控制臺(tái),就是在后臺(tái)新增一個(gè)策略,這個(gè)策略是鏡像集群模式的策略,指定的時(shí)候是可以要求數(shù)據(jù)同步到所有節(jié)點(diǎn)的,也可以要求同步到指定數(shù)量的節(jié)點(diǎn),再次創(chuàng)建 queue 的時(shí)候,應(yīng)用這個(gè)策略,就會(huì)自動(dòng)將數(shù)據(jù)同步到其他的節(jié)點(diǎn)上去了。
這樣的話,好處在于,你任何一個(gè)機(jī)器宕機(jī)了,沒(méi)事兒,其它機(jī)器(節(jié)點(diǎn))還包含了這個(gè) queue 的完整數(shù)據(jù),別的 consumer 都可以到其它節(jié)點(diǎn)上去消費(fèi)數(shù)據(jù)。壞處在于,第一,這個(gè)性能開(kāi)銷也太大了吧,消息需要同步到所有機(jī)器上,導(dǎo)致網(wǎng)絡(luò)帶寬壓力和消耗很重!第二,這么玩兒,不是分布式的,就沒(méi)有擴(kuò)展性可言了,如果某個(gè) queue 負(fù)載很重,你加機(jī)器,新增的機(jī)器也包含了這個(gè) queue 的所有數(shù)據(jù),并沒(méi)有辦法線性擴(kuò)展你的 queue。你想,如果這個(gè) queue 的數(shù)據(jù)量很大,大到這個(gè)機(jī)器上的容量無(wú)法容納了,此時(shí)該怎么辦呢?
RocketMQ 的高可用性
RcoketMQ的集群有:多master 模式、多master多slave異步復(fù)制模式、多 master多slave同步雙寫模式。
多master多slave模式部署架構(gòu)圖:

通信過(guò)程如下:Producer 與 NameServer集群中的其中一個(gè)節(jié)點(diǎn)(隨機(jī)選擇)建立長(zhǎng)連接,定期從 NameServer 獲取 Topic 路由信息,并向提供 Topic 服務(wù)的 Broker Master 建立長(zhǎng)連接,且定時(shí)向 Broker 發(fā)送心跳。Producer 只能將消息發(fā)送到 Broker master,但是 Consumer 則不一樣,它同時(shí)和提供 Topic 服務(wù)的 Master 和 Slave建立長(zhǎng)連接,既可以從 Broker Master 訂閱消息,也可以從 Broker Slave 訂閱消息。
Kafka 的高可用性
Kafka 一個(gè)最基本的架構(gòu)認(rèn)識(shí):由多個(gè) broker 組成,每個(gè) broker 是一個(gè)節(jié)點(diǎn);你創(chuàng)建一個(gè) topic,這個(gè) topic 可以劃分為多個(gè) partition,每個(gè) partition 可以存在于不同的 broker 上,每個(gè) partition 就放一部分?jǐn)?shù)據(jù)。
這就是天然的分布式消息隊(duì)列,就是說(shuō)一個(gè) topic 的數(shù)據(jù),是分散放在多個(gè)機(jī)器上的,每個(gè)機(jī)器就放一部分?jǐn)?shù)據(jù)。
實(shí)際上 RabbitMQ 之類的,并不是分布式消息隊(duì)列,它就是傳統(tǒng)的消息隊(duì)列,只不過(guò)提供了一些集群、HA(High Availability, 高可用性) 的機(jī)制而已,因?yàn)闊o(wú)論怎么玩兒,RabbitMQ 一個(gè) queue 的數(shù)據(jù)都是放在一個(gè)節(jié)點(diǎn)里的,鏡像集群下,也是每個(gè)節(jié)點(diǎn)都放這個(gè) queue 的完整數(shù)據(jù)。
Kafka 0.8 以前,是沒(méi)有 HA 機(jī)制的,就是任何一個(gè) broker 宕機(jī)了,那個(gè) broker 上的 partition 就廢了,沒(méi)法寫也沒(méi)法讀,沒(méi)有什么高可用性可言。
比如說(shuō),我們假設(shè)創(chuàng)建了一個(gè) topic,指定其 partition 數(shù)量是 3 個(gè),分別在三臺(tái)機(jī)器上。但是,如果第二臺(tái)機(jī)器宕機(jī)了,會(huì)導(dǎo)致這個(gè) topic 的 1/3 的數(shù)據(jù)就丟了,因此這個(gè)是做不到高可用的。

Kafka 0.8 以后,提供了 HA 機(jī)制,就是 replica(復(fù)制品) 副本機(jī)制。每個(gè) partition 的數(shù)據(jù)都會(huì)同步到其它機(jī)器上,形成自己的多個(gè) replica 副本。所有 replica 會(huì)選舉一個(gè) leader 出來(lái),那么生產(chǎn)和消費(fèi)都跟這個(gè) leader 打交道,然后其他 replica 就是 follower。寫的時(shí)候,leader 會(huì)負(fù)責(zé)把數(shù)據(jù)同步到所有 follower 上去,讀的時(shí)候就直接讀 leader 上的數(shù)據(jù)即可。只能讀寫 leader?很簡(jiǎn)單,要是你可以隨意讀寫每個(gè) follower,那么就要 care 數(shù)據(jù)一致性的問(wèn)題,系統(tǒng)復(fù)雜度太高,很容易出問(wèn)題。Kafka 會(huì)均勻地將一個(gè) partition 的所有 replica 分布在不同的機(jī)器上,這樣才可以提高容錯(cuò)性。

這么搞,就有所謂的高可用性了,因?yàn)槿绻硞€(gè) broker 宕機(jī)了,沒(méi)事兒,那個(gè) broker上面的 partition 在其他機(jī)器上都有副本的。如果這個(gè)宕機(jī)的 broker 上面有某個(gè) partition 的 leader,那么此時(shí)會(huì)從 follower 中重新選舉一個(gè)新的 leader 出來(lái),大家繼續(xù)讀寫那個(gè)新的 leader 即可。這就有所謂的高可用性了。
寫數(shù)據(jù)的時(shí)候,生產(chǎn)者就寫 leader,然后 leader 將數(shù)據(jù)落地寫本地磁盤,接著其他 follower 自己主動(dòng)從 leader 來(lái) pull 數(shù)據(jù)。一旦所有 follower 同步好數(shù)據(jù)了,就會(huì)發(fā)送 ack 給 leader,leader 收到所有 follower 的 ack 之后,就會(huì)返回寫成功的消息給生產(chǎn)者。(當(dāng)然,這只是其中一種模式,還可以適當(dāng)調(diào)整這個(gè)行為)
消費(fèi)的時(shí)候,只會(huì)從 leader 去讀,但是只有當(dāng)一個(gè)消息已經(jīng)被所有 follower 都同步成功返回 ack 的時(shí)候,這個(gè)消息才會(huì)被消費(fèi)者讀到。
如何保證消息不被重復(fù)消費(fèi)?
回答這個(gè)問(wèn)題,首先你別聽(tīng)到重復(fù)消息這個(gè)事兒,就一無(wú)所知吧,你先大概說(shuō)一說(shuō)可能會(huì)有哪些重復(fù)消費(fèi)的問(wèn)題。
首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能會(huì)出現(xiàn)消息重復(fù)消費(fèi)的問(wèn)題,正常。因?yàn)檫@問(wèn)題通常不是 MQ 自己保證的,是由我們開(kāi)發(fā)來(lái)保證的。挑一個(gè) Kafka 來(lái)舉個(gè)例子,說(shuō)說(shuō)怎么重復(fù)消費(fèi)吧。
Kafka 實(shí)際上有個(gè) offset 的概念,就是每個(gè)消息寫進(jìn)去,都有一個(gè) offset,代表消息的序號(hào),然后 consumer 消費(fèi)了數(shù)據(jù)之后,每隔一段時(shí)間(定時(shí)定期),會(huì)把自己消費(fèi)過(guò)的消息的 offset 提交一下,表示“我已經(jīng)消費(fèi)過(guò)了,下次我要是重啟啥的,你就讓我繼續(xù)從上次消費(fèi)到的 offset 來(lái)繼續(xù)消費(fèi)吧”。
但是凡事總有意外,比如我們之前生產(chǎn)經(jīng)常遇到的,就是你有時(shí)候重啟系統(tǒng),看你怎么重啟了,如果碰到點(diǎn)著急的,直接 kill 進(jìn)程了,再重啟。這會(huì)導(dǎo)致 consumer 有些消息處理了,但是沒(méi)來(lái)得及提交 offset,尷尬了。重啟之后,少數(shù)消息會(huì)再次消費(fèi)一次。
舉個(gè)栗子。
有這么個(gè)場(chǎng)景。數(shù)據(jù) 1/2/3 依次進(jìn)入 kafka,kafka 會(huì)給這三條數(shù)據(jù)每條分配一個(gè) offset,代表這條數(shù)據(jù)的序號(hào),我們就假設(shè)分配的 offset 依次是 152/153/154。消費(fèi)者從 kafka 去消費(fèi)的時(shí)候,也是按照這個(gè)順序去消費(fèi)。假如當(dāng)消費(fèi)者消費(fèi)了 offset=153 的這條數(shù)據(jù),剛準(zhǔn)備去提交 offset 到 zookeeper,此時(shí)消費(fèi)者進(jìn)程被重啟了。那么此時(shí)消費(fèi)過(guò)的數(shù)據(jù) 1/2 的 offset 并沒(méi)有提交,kafka 也就不知道你已經(jīng)消費(fèi)了 offset=153 這條數(shù)據(jù)。那么重啟之后,消費(fèi)者會(huì)找 kafka 說(shuō),嘿,哥兒們,你給我接著把上次我消費(fèi)到的那個(gè)地方后面的數(shù)據(jù)繼續(xù)給我傳遞過(guò)來(lái)。由于之前的 offset 沒(méi)有提交成功,那么數(shù)據(jù) 1/2 會(huì)再次傳過(guò)來(lái),如果此時(shí)消費(fèi)者沒(méi)有去重的話,那么就會(huì)導(dǎo)致重復(fù)消費(fèi)。

如果消費(fèi)者干的事兒是拿一條數(shù)據(jù)就往數(shù)據(jù)庫(kù)里寫一條,會(huì)導(dǎo)致說(shuō),你可能就把數(shù)據(jù) 1/2 在數(shù)據(jù)庫(kù)里插入了 2 次,那么數(shù)據(jù)就錯(cuò)啦。
其實(shí)重復(fù)消費(fèi)不可怕,可怕的是你沒(méi)考慮到重復(fù)消費(fèi)之后,怎么保證冪等性。
舉個(gè)例子吧。假設(shè)你有個(gè)系統(tǒng),消費(fèi)一條消息就往數(shù)據(jù)庫(kù)里插入一條數(shù)據(jù),要是你一個(gè)消息重復(fù)兩次,你不就插入了兩條,這數(shù)據(jù)不就錯(cuò)了?但是你要是消費(fèi)到第二次的時(shí)候,自己判斷一下是否已經(jīng)消費(fèi)過(guò)了,若是就直接扔了,這樣不就保留了一條數(shù)據(jù),從而保證了數(shù)據(jù)的正確性。
一條數(shù)據(jù)重復(fù)出現(xiàn)兩次,數(shù)據(jù)庫(kù)里就只有一條數(shù)據(jù),這就保證了系統(tǒng)的冪等性。
冪等性,通俗點(diǎn)說(shuō),就一個(gè)數(shù)據(jù),或者一個(gè)請(qǐng)求,給你重復(fù)來(lái)多次,你得確保對(duì)應(yīng)的數(shù)據(jù)是不會(huì)改變的,不能出錯(cuò)。
所以第二個(gè)問(wèn)題來(lái)了,怎么保證消息隊(duì)列消費(fèi)的冪等性?
其實(shí)還是得結(jié)合業(yè)務(wù)來(lái)思考,我這里給幾個(gè)思路:
- 比如你拿個(gè)數(shù)據(jù)要寫庫(kù),你先根據(jù)主鍵查一下,如果這數(shù)據(jù)都有了,你就別插入了,update 一下好吧。
- 比如你是寫 Redis,那沒(méi)問(wèn)題了,反正每次都是 set,天然冪等性。
- 比如你不是上面兩個(gè)場(chǎng)景,那做的稍微復(fù)雜一點(diǎn),你需要讓生產(chǎn)者發(fā)送每條數(shù)據(jù)的時(shí)候,里面加一個(gè)全局唯一的 id,類似訂單 id 之類的東西,然后你這里消費(fèi)到了之后,先根據(jù)這個(gè) id 去比如 Redis 里查一下,之前消費(fèi)過(guò)嗎?如果沒(méi)有消費(fèi)過(guò),你就處理,然后這個(gè) id 寫 Redis。如果消費(fèi)過(guò)了,那你就別處理了,保證別重復(fù)處理相同的消息即可。
- 比如基于數(shù)據(jù)庫(kù)的唯一鍵來(lái)保證重復(fù)數(shù)據(jù)不會(huì)重復(fù)插入多條。因?yàn)橛形ㄒ绘I約束了,重復(fù)數(shù)據(jù)插入只會(huì)報(bào)錯(cuò),不會(huì)導(dǎo)致數(shù)據(jù)庫(kù)中出現(xiàn)臟數(shù)據(jù)。
當(dāng)然,如何保證 MQ 的消費(fèi)是冪等性的,需要結(jié)合具體的業(yè)務(wù)來(lái)看。
如何保證消息的可靠傳輸(處理消息丟失)?
RabbitMQ

生產(chǎn)者弄丟數(shù)據(jù)
生產(chǎn)者將數(shù)據(jù)發(fā)送到 RabbitMQ 的時(shí)候,可能數(shù)據(jù)就在半路給搞丟了,因?yàn)榫W(wǎng)絡(luò)問(wèn)題啥的,都有可能。
此時(shí)可以選擇用 RabbitMQ 提供的事務(wù)功能,就是生產(chǎn)者發(fā)送數(shù)據(jù)之前開(kāi)啟 RabbitMQ 事務(wù)channel.txSelect,然后發(fā)送消息,如果消息沒(méi)有成功被 RabbitMQ 接收到,那么生產(chǎn)者會(huì)收到異常報(bào)錯(cuò),此時(shí)就可以回滾事務(wù)channel.txRollback,然后重試發(fā)送消息;如果收到了消息,那么可以提交事務(wù)channel.txCommit。
// 開(kāi)啟事務(wù)
channel.txSelect
try {
// 這里發(fā)送消息
} catch (Exception e) {
channel.txRollback
// 這里再次重發(fā)這條消息
} // 提交事務(wù)
channel.txCommit
但是問(wèn)題是,RabbitMQ 事務(wù)機(jī)制(同步)一搞,基本上吞吐量會(huì)下來(lái),因?yàn)樘男阅堋?/p>
所以一般來(lái)說(shuō),如果你要確保說(shuō)寫 RabbitMQ 的消息別丟,可以開(kāi)啟 confirm 模式,在生產(chǎn)者那里設(shè)置開(kāi)啟 confirm 模式之后,你每次寫的消息都會(huì)分配一個(gè)唯一的 id,然后如果寫入了 RabbitMQ 中,RabbitMQ 會(huì)給你回傳一個(gè) ack 消息,告訴你說(shuō)這個(gè)消息 ok 了。如果 RabbitMQ 沒(méi)能處理這個(gè)消息,會(huì)回調(diào)你的一個(gè) nack 接口,告訴你這個(gè)消息接收失敗,你可以重試。而且你可以結(jié)合這個(gè)機(jī)制自己在內(nèi)存里維護(hù)每個(gè)消息 id 的狀態(tài),如果超過(guò)一定時(shí)間還沒(méi)接收到這個(gè)消息的回調(diào),那么你可以重發(fā)。
事務(wù)機(jī)制和 confirm 機(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 自己弄丟了數(shù)據(jù),這個(gè)你必須開(kāi)啟 RabbitMQ 的持久化,就是消息寫入之后會(huì)持久化到磁盤,哪怕是 RabbitMQ 自己掛了,恢復(fù)之后會(huì)自動(dòng)讀取之前存儲(chǔ)的數(shù)據(jù),一般數(shù)據(jù)不會(huì)丟。除非極其罕見(jiàn)的是,RabbitMQ 還沒(méi)持久化,自己就掛了,可能導(dǎo)致少量數(shù)據(jù)丟失,但是這個(gè)概率較小。
設(shè)置持久化有兩個(gè)步驟:
- 創(chuàng)建 queue 的時(shí)候?qū)⑵湓O(shè)置為持久化
這樣就可以保證 RabbitMQ 持久化 queue 的元數(shù)據(jù),但是它是不會(huì)持久化 queue 里的數(shù)據(jù)的。 - 第二個(gè)是發(fā)送消息的時(shí)候?qū)⑾⒌?
deliveryMode設(shè)置為 2
就是將消息設(shè)置為持久化的,此時(shí) RabbitMQ 就會(huì)將消息持久化到磁盤上去。
必須要同時(shí)設(shè)置這兩個(gè)持久化才行,RabbitMQ 哪怕是掛了,再次重啟,也會(huì)從磁盤上重啟恢復(fù) queue,恢復(fù)這個(gè) queue 里的數(shù)據(jù)。
注意,哪怕是你給 RabbitMQ 開(kāi)啟了持久化機(jī)制,也有一種可能,就是這個(gè)消息寫到了 RabbitMQ 中,但是還沒(méi)來(lái)得及持久化到磁盤上,結(jié)果不巧,此時(shí) RabbitMQ 掛了,就會(huì)導(dǎo)致內(nèi)存里的一點(diǎn)點(diǎn)數(shù)據(jù)丟失。
所以,持久化可以跟生產(chǎn)者那邊的 confirm 機(jī)制配合起來(lái),只有消息被持久化到磁盤之后,才會(huì)通知生產(chǎn)者 ack 了,所以哪怕是在持久化到磁盤之前,RabbitMQ 掛了,數(shù)據(jù)丟了,生產(chǎn)者收不到 ack,你也是可以自己重發(fā)的。
消費(fèi)端弄丟數(shù)據(jù)
RabbitMQ 如果丟失了數(shù)據(jù),主要是因?yàn)槟阆M(fèi)的時(shí)候,剛消費(fèi)到,還沒(méi)處理,結(jié)果進(jìn)程掛了,比如重啟了,那么就尷尬了,RabbitMQ 認(rèn)為你都消費(fèi)了,這數(shù)據(jù)就丟了。
這個(gè)時(shí)候得用 RabbitMQ 提供的 ack 機(jī)制,簡(jiǎn)單來(lái)說(shuō),就是你必須關(guān)閉 RabbitMQ 的自動(dòng) ack,可以通過(guò)一個(gè) api 來(lái)調(diào)用就行,然后每次你自己代碼里確保處理完的時(shí)候,再在程序里 ack 一把。這樣的話,如果你還沒(méi)處理完,不就沒(méi)有 ack 了?那 RabbitMQ 就認(rèn)為你還沒(méi)處理完,這個(gè)時(shí)候 RabbitMQ 會(huì)把這個(gè)消費(fèi)分配給別的 consumer 去處理,消息是不會(huì)丟的。

Kafka
先引一張Kafka Replication數(shù)據(jù)流向圖

消費(fèi)端弄丟數(shù)據(jù)
唯一可能導(dǎo)致消費(fèi)者弄丟數(shù)據(jù)的情況,就是說(shuō),你消費(fèi)到了這個(gè)消息,然后消費(fèi)者那邊自動(dòng)提交了 offset,讓 Kafka 以為你已經(jīng)消費(fèi)好了這個(gè)消息,但其實(shí)你才剛準(zhǔn)備處理這個(gè)消息,你還沒(méi)處理,你自己就掛了,此時(shí)這條消息就丟咯。
這不是跟 RabbitMQ 差不多嗎,大家都知道 Kafka 會(huì)自動(dòng)提交 offset,那么只要關(guān)閉自動(dòng)提交 offset,在處理完之后自己手動(dòng)提交 offset,就可以保證數(shù)據(jù)不會(huì)丟。但是此時(shí)確實(shí)還是可能會(huì)有重復(fù)消費(fèi),比如你剛處理完,還沒(méi)提交 offset,結(jié)果自己掛了,此時(shí)肯定會(huì)重復(fù)消費(fèi)一次,自己保證冪等性就好了。
生產(chǎn)環(huán)境碰到的一個(gè)問(wèn)題,就是說(shuō)我們的 Kafka 消費(fèi)者消費(fèi)到了數(shù)據(jù)之后是寫到一個(gè)內(nèi)存的 queue 里先緩沖一下,結(jié)果有的時(shí)候,你剛把消息寫入內(nèi)存 queue,然后消費(fèi)者會(huì)自動(dòng)提交 offset。然后此時(shí)我們重啟了系統(tǒng),就會(huì)導(dǎo)致內(nèi)存 queue 里還沒(méi)來(lái)得及處理的數(shù)據(jù)就丟失了。
Kafka弄丟數(shù)據(jù)
這塊比較常見(jiàn)的一個(gè)場(chǎng)景,就是 Kafka 某個(gè) broker 宕機(jī),然后重新選舉 partition 的 leader。大家想想,要是此時(shí)其他的 follower 剛好還有些數(shù)據(jù)沒(méi)有同步,結(jié)果此時(shí) leader 掛了,然后選舉某個(gè) follower 成 leader 之后,不就少了一些數(shù)據(jù)?這就丟了一些數(shù)據(jù)啊。
生產(chǎn)環(huán)境也遇到過(guò),我們也是,之前 Kafka 的 leader 機(jī)器宕機(jī)了,將 follower 切換為 leader 之后,就會(huì)發(fā)現(xiàn)說(shuō)這個(gè)數(shù)據(jù)就丟了。
所以此時(shí)一般是要求起碼設(shè)置如下 4 個(gè)參數(shù):
- 給 topic 設(shè)置
replication.factor參數(shù):這個(gè)值必須大于 1,要求每個(gè) partition 必須有至少 2 個(gè)副本。 - 在 Kafka 服務(wù)端設(shè)置
min.insync.replicas參數(shù):這個(gè)值必須大于 1,這個(gè)是要求一個(gè) leader 至少感知到有至少一個(gè) follower 還跟自己保持聯(lián)系,沒(méi)掉隊(duì),這樣才能確保 leader 掛了還有一個(gè) follower 吧。 - 在 producer 端設(shè)置
acks=all:這個(gè)是要求每條數(shù)據(jù),必須是寫入所有 replica 之后,才能認(rèn)為是寫成功了。 - 在 producer 端設(shè)置
retries=MAX(很大很大很大的一個(gè)值,無(wú)限次重試的意思):這個(gè)是要求一旦寫入失敗,就無(wú)限重試,卡在這里了。
我們生產(chǎn)環(huán)境就是按照上述要求配置的,這樣配置之后,至少在 Kafka broker 端就可以保證在 leader 所在 broker 發(fā)生故障,進(jìn)行 leader 切換時(shí),數(shù)據(jù)不會(huì)丟失。
生產(chǎn)者丟數(shù)據(jù)
如果按照上述的思路設(shè)置了 acks=all,一定不會(huì)丟,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才認(rèn)為本次寫成功了。如果沒(méi)滿足這個(gè)條件,生產(chǎn)者會(huì)自動(dòng)不斷的重試,重試無(wú)限次。
如何保證消息的順序?
我舉個(gè)例子,我們以前做過(guò)一個(gè) mysql binlog 同步的系統(tǒng),壓力還是非常大的,日同步數(shù)據(jù)要達(dá)到上億,就是說(shuō)數(shù)據(jù)從一個(gè) mysql 庫(kù)原封不動(dòng)地同步到另一個(gè) mysql 庫(kù)里面去(mysql -> mysql)。常見(jiàn)的一點(diǎn)在于說(shuō)比如大數(shù)據(jù) team,就需要同步一個(gè) mysql 庫(kù)過(guò)來(lái),對(duì)公司的業(yè)務(wù)系統(tǒng)的數(shù)據(jù)做各種復(fù)雜的操作。
你在 mysql 里增刪改一條數(shù)據(jù),對(duì)應(yīng)出來(lái)了增刪改 3 條 binlog 日志,接著這三條 binlog 發(fā)送到 MQ 里面,再消費(fèi)出來(lái)依次執(zhí)行,起碼得保證人家是按照順序來(lái)的吧?不然本來(lái)是:增加、修改、刪除;你愣是換了順序給執(zhí)行成刪除、修改、增加,不全錯(cuò)了么。
本來(lái)這個(gè)數(shù)據(jù)同步過(guò)來(lái),應(yīng)該最后這個(gè)數(shù)據(jù)被刪除了;結(jié)果你搞錯(cuò)了這個(gè)順序,最后這個(gè)數(shù)據(jù)保留下來(lái)了,數(shù)據(jù)同步就出錯(cuò)了。
先看看順序會(huì)錯(cuò)亂的倆場(chǎng)景:
RabbitMQ:一個(gè) queue,多個(gè) consumer。比如,生產(chǎn)者向 RabbitMQ 里發(fā)送了三條數(shù)據(jù),順序依次是 data1/data2/data3,壓入的是 RabbitMQ 的一個(gè)內(nèi)存隊(duì)列。有三個(gè)消費(fèi)者分別從 MQ 中消費(fèi)這三條數(shù)據(jù)中的一條,結(jié)果消費(fèi)者2先執(zhí)行完操作,把 data2 存入數(shù)據(jù)庫(kù),然后是 data1/data3。這不明顯亂了。

Kafka:比如說(shuō)我們建了一個(gè) topic,有三個(gè) partition。生產(chǎn)者在寫的時(shí)候,其實(shí)可以指定一個(gè) key,比如說(shuō)我們指定了某個(gè)訂單 id 作為 key,那么這個(gè)訂單相關(guān)的數(shù)據(jù),一定會(huì)被分發(fā)到同一個(gè) partition 中去,而且這個(gè) partition 中的數(shù)據(jù)一定是有順序的。
消費(fèi)者從 partition 中取出來(lái)數(shù)據(jù)的時(shí)候,也一定是有順序的。到這里,順序還是 ok 的,沒(méi)有錯(cuò)亂。接著,我們?cè)谙M(fèi)者里可能會(huì)搞多個(gè)線程來(lái)并發(fā)處理消息。因?yàn)槿绻M(fèi)者是單線程消費(fèi)處理,而處理比較耗時(shí)的話,比如處理一條消息耗時(shí)幾十 ms,那么 1 秒鐘只能處理幾十條消息,這吞吐量太低了。而多個(gè)線程并發(fā)跑的話,順序可能就亂掉了。

解決方案
RabbitMQ
拆分多個(gè) queue,每個(gè) queue 一個(gè) consumer,就是多一些 queue 而已,確實(shí)是麻煩點(diǎn);或者就一個(gè) queue 但是對(duì)應(yīng)一個(gè) consumer,然后這個(gè) consumer 內(nèi)部用內(nèi)存隊(duì)列做排隊(duì),然后分發(fā)給底層不同的 worker 來(lái)處理。

Kafka
- 一個(gè) topic,一個(gè) partition,一個(gè) consumer,內(nèi)部單線程消費(fèi),單線程吞吐量太低,一般不會(huì)用這個(gè)。
- 寫 N 個(gè)內(nèi)存 queue,具有相同 key 的數(shù)據(jù)都到同一個(gè)內(nèi)存 queue;然后對(duì)于 N 個(gè)線程,每個(gè)線程分別消費(fèi)一個(gè)內(nèi)存 queue 即可,這樣就能保證順序性。

如何解決消息隊(duì)列的延時(shí)以及過(guò)期失效問(wèn)題?消息隊(duì)列滿了以后該怎么處理?有幾百萬(wàn)消息持續(xù)積壓幾小時(shí),說(shuō)說(shuō)怎么解決?
你看這問(wèn)法,其實(shí)本質(zhì)針對(duì)的場(chǎng)景,都是說(shuō),可能你的消費(fèi)端出了問(wèn)題,不消費(fèi)了;或者消費(fèi)的速度極其慢。接著就坑爹了,可能你的消息隊(duì)列集群的磁盤都快寫滿了,都沒(méi)人消費(fèi),這個(gè)時(shí)候怎么辦?或者是這整個(gè)就積壓了幾個(gè)小時(shí),你這個(gè)時(shí)候怎么辦?或者是你積壓的時(shí)間太長(zhǎng)了,導(dǎo)致比如 RabbitMQ 設(shè)置了消息過(guò)期時(shí)間后就沒(méi)了怎么辦?
所以就這事兒,其實(shí)線上挺常見(jiàn)的,一般不出,一出就是大 case。一般常見(jiàn)于,舉個(gè)例子,消費(fèi)端每次消費(fèi)之后要寫 mysql,結(jié)果 mysql 掛了,消費(fèi)端 hang 那兒了,不動(dòng)了;或者是消費(fèi)端出了個(gè)什么岔子,導(dǎo)致消費(fèi)速度極其慢。
大量消息在 mq 里積壓了幾個(gè)小時(shí)了還沒(méi)解決
幾千萬(wàn)條數(shù)據(jù)在 MQ 里積壓了七八個(gè)小時(shí),從下午 4 點(diǎn)多,積壓到了晚上 11 點(diǎn)多。這個(gè)是我們真實(shí)遇到過(guò)的一個(gè)場(chǎng)景,確實(shí)是線上故障了,這個(gè)時(shí)候要不然就是修復(fù) consumer 的問(wèn)題,讓它恢復(fù)消費(fèi)速度,然后傻傻的等待幾個(gè)小時(shí)消費(fèi)完畢。這個(gè)肯定不能在面試的時(shí)候說(shuō)吧。
一個(gè)消費(fèi)者一秒是 1000 條,一秒 3 個(gè)消費(fèi)者是 3000 條,一分鐘就是 18 萬(wàn)條。所以如果你積壓了幾百萬(wàn)到上千萬(wàn)的數(shù)據(jù),即使消費(fèi)者恢復(fù)了,也需要大概 1 小時(shí)的時(shí)間才能恢復(fù)過(guò)來(lái)。
一般這個(gè)時(shí)候,只能臨時(shí)緊急擴(kuò)容了,具體操作步驟和思路如下:
- 先修復(fù) consumer 的問(wèn)題,確保其恢復(fù)消費(fèi)速度,然后將現(xiàn)有 consumer 都停掉。
- 新建一個(gè) topic,partition 是原來(lái)的 10 倍,臨時(shí)建立好原先 10 倍的 queue 數(shù)量。
- 然后寫一個(gè)臨時(shí)的分發(fā)數(shù)據(jù)的 consumer 程序,這個(gè)程序部署上去消費(fèi)積壓的數(shù)據(jù),消費(fèi)之后不做耗時(shí)的處理,直接均勻輪詢寫入臨時(shí)建立好的 10 倍數(shù)量的 queue。
- 接著臨時(shí)征用 10 倍的機(jī)器來(lái)部署 consumer,每一批 consumer 消費(fèi)一個(gè)臨時(shí) queue 的數(shù)據(jù)。這種做法相當(dāng)于是臨時(shí)將 queue 資源和 consumer 資源擴(kuò)大 10 倍,以正常的 10 倍速度來(lái)消費(fèi)數(shù)據(jù)。
- 等快速消費(fèi)完積壓數(shù)據(jù)之后,得恢復(fù)原先部署的架構(gòu),重新用原先的 consumer 機(jī)器來(lái)消費(fèi)消息。
mq 中的消息過(guò)期失效了
假設(shè)你用的是 RabbitMQ,RabbtiMQ 是可以設(shè)置過(guò)期時(shí)間的,也就是 TTL。如果消息在 queue 中積壓超過(guò)一定的時(shí)間就會(huì)被 RabbitMQ 給清理掉,這個(gè)數(shù)據(jù)就沒(méi)了。那這就是第二個(gè)坑了。這就不是說(shuō)數(shù)據(jù)會(huì)大量積壓在 mq 里,而是大量的數(shù)據(jù)會(huì)直接搞丟。
這個(gè)情況下,就不是說(shuō)要增加 consumer 消費(fèi)積壓的消息,因?yàn)閷?shí)際上沒(méi)啥積壓,而是丟了大量的消息。我們可以采取一個(gè)方案,就是批量重導(dǎo),這個(gè)我們之前線上也有類似的場(chǎng)景干過(guò)。就是大量積壓的時(shí)候,我們當(dāng)時(shí)就直接丟棄數(shù)據(jù)了,然后等過(guò)了高峰期以后,比如大家一起喝咖啡熬夜到晚上12點(diǎn)以后,用戶都睡覺(jué)了。這個(gè)時(shí)候我們就開(kāi)始寫程序,將丟失的那批數(shù)據(jù),寫個(gè)臨時(shí)程序,一點(diǎn)一點(diǎn)的查出來(lái),然后重新灌入 mq 里面去,把白天丟的數(shù)據(jù)給他補(bǔ)回來(lái)。也只能是這樣了。
假設(shè) 1 萬(wàn)個(gè)訂單積壓在 mq 里面,沒(méi)有處理,其中 1000 個(gè)訂單都丟了,你只能手動(dòng)寫程序把那 1000 個(gè)訂單給查出來(lái),手動(dòng)發(fā)到 mq 里去再補(bǔ)一次。
mq 都快寫滿了
如果消息積壓在 mq 里,你很長(zhǎng)時(shí)間都沒(méi)有處理掉,此時(shí)導(dǎo)致 mq 都快寫滿了,咋辦?這個(gè)還有別的辦法嗎?沒(méi)有,誰(shuí)讓你第一個(gè)方案執(zhí)行的太慢了,你臨時(shí)寫程序,接入數(shù)據(jù)來(lái)消費(fèi),消費(fèi)一個(gè)丟棄一個(gè),都不要了,快速消費(fèi)掉所有的消息。然后走第二個(gè)方案,到了晚上再補(bǔ)數(shù)據(jù)吧。
如果讓你寫一個(gè)消息隊(duì)列,該如何進(jìn)行架構(gòu)設(shè)計(jì)?說(shuō)一下你的思路。
其實(shí)聊到這個(gè)問(wèn)題,一般面試官要考察兩塊:
- 你有沒(méi)有對(duì)某一個(gè)消息隊(duì)列做過(guò)較為深入的原理的了解,或者從整體了解把握住一個(gè)消息隊(duì)列的架構(gòu)原理。
- 看看你的設(shè)計(jì)能力,給你一個(gè)常見(jiàn)的系統(tǒng),就是消息隊(duì)列系統(tǒng),看看你能不能從全局把握一下整體架構(gòu)設(shè)計(jì),給出一些關(guān)鍵點(diǎn)出來(lái)。
說(shuō)實(shí)話,問(wèn)類似問(wèn)題的時(shí)候,大部分人基本都會(huì)蒙,因?yàn)槠綍r(shí)從來(lái)沒(méi)有思考過(guò)類似的問(wèn)題,大多數(shù)人就是平時(shí)埋頭用,從來(lái)不去思考背后的一些東西。類似的問(wèn)題,比如,如果讓你來(lái)設(shè)計(jì)一個(gè) Spring 框架你會(huì)怎么做?如果讓你來(lái)設(shè)計(jì)一個(gè) Dubbo 框架你會(huì)怎么做?如果讓你來(lái)設(shè)計(jì)一個(gè) MyBatis 框架你會(huì)怎么做?
其實(shí)回答這類問(wèn)題,說(shuō)白了,不求你看過(guò)那技術(shù)的源碼,起碼你要大概知道那個(gè)技術(shù)的基本原理、核心組成部分、基本架構(gòu)構(gòu)成,然后參照一些開(kāi)源的技術(shù)把一個(gè)系統(tǒng)設(shè)計(jì)出來(lái)的思路說(shuō)一下就好。
比如說(shuō)這個(gè)消息隊(duì)列系統(tǒng),我們從以下幾個(gè)角度來(lái)考慮一下:
首先這個(gè) mq 得支持可伸縮性吧,就是需要的時(shí)候快速擴(kuò)容,就可以增加吞吐量和容量,那怎么搞?設(shè)計(jì)個(gè)分布式的系統(tǒng)唄,參照一下 kafka 的設(shè)計(jì)理念,broker -> topic -> partition,每個(gè) partition 放一個(gè)機(jī)器,就存一部分?jǐn)?shù)據(jù)。如果現(xiàn)在資源不夠了,簡(jiǎn)單啊,給 topic 增加 partition,然后做數(shù)據(jù)遷移,增加機(jī)器,不就可以存放更多數(shù)據(jù),提供更高的吞吐量了?
其次你得考慮一下這個(gè) mq 的數(shù)據(jù)要不要落地磁盤吧?那肯定要了,落磁盤才能保證別進(jìn)程掛了數(shù)據(jù)就丟了。那落磁盤的時(shí)候怎么落???順序?qū)懀@樣就沒(méi)有磁盤隨機(jī)讀寫的尋址開(kāi)銷,磁盤順序讀寫的性能是很高的,這就是 kafka 的思路。
其次你考慮一下你的 mq 的可用性???這個(gè)事兒,具體參考之前可用性那個(gè)環(huán)節(jié)講解的 kafka 的高可用保障機(jī)制。多副本 -> leader & follower -> broker 掛了重新選舉 leader 即可對(duì)外服務(wù)。
能不能支持?jǐn)?shù)據(jù) 0 丟失?。靠梢缘模瑓⒖嘉覀冎罢f(shuō)的那個(gè) kafka 數(shù)據(jù)零丟失方案。
mq 肯定是很復(fù)雜的,面試官問(wèn)你這個(gè)問(wèn)題,其實(shí)是個(gè)開(kāi)放題,他就是看看你有沒(méi)有從架構(gòu)角度整體構(gòu)思和設(shè)計(jì)的思維以及能力。確實(shí)這個(gè)問(wèn)題可以刷掉一大批人,因?yàn)榇蟛糠秩似綍r(shí)不思考這些東西。
Redis做消息隊(duì)列與其他消息隊(duì)列相比有什么不同
Redis作為消息隊(duì)列:
- 如果你的需求是快產(chǎn)快消的即時(shí)消費(fèi)場(chǎng)景,并且生產(chǎn)的消息立即被消費(fèi)者消費(fèi)掉。
- 如果速度是你十分看重的,比如慢了一秒好幾千萬(wàn)這種。
- 如果允許出現(xiàn)消息丟失的場(chǎng)景。
- 如果你不需要系統(tǒng)保存你發(fā)送過(guò)的消息。
- 如果需要處理的數(shù)據(jù)量并不是那么巨大。
其他消息隊(duì)列:
- 如果你想要穩(wěn)定的消息隊(duì)列。
- 如果你想要你發(fā)送過(guò)的消息可以保留一定的時(shí)間,并不是無(wú)跡可尋的時(shí)候。
- 如果你無(wú)法忍受數(shù)據(jù)的丟失。
- 如果速度不需要那么的快。
- 如果需要處理數(shù)據(jù)量巨大的時(shí)候。
應(yīng)用場(chǎng)景分析
Redis:輕量級(jí),高并發(fā),延遲敏感
即時(shí)數(shù)據(jù)分析、秒殺計(jì)數(shù)器、緩存等。
其他MQ:重量級(jí),高并發(fā),異步
批量數(shù)據(jù)異步處理、并行任務(wù)串行化,高負(fù)載任務(wù)的負(fù)載均衡等。