MQ問題梳理轉(zhuǎn)自陳琰AC

一、為什么使用 MQ?

1.1 解耦

1.1.1 解耦

例如電商系統(tǒng)核心是交易服務(wù),交易服務(wù)要調(diào)用另外三個服務(wù),訂單服務(wù)、庫存服務(wù)、倉儲服務(wù)。


image.png

這三個服務(wù)如果有一個服務(wù)不可用,交易服務(wù)就無法正常運行,所以交易服務(wù)是強耦合另外三個服務(wù)。

引入MQ之后,交易服務(wù)只跟MQ交互,把消息發(fā)到MQ里面就行了,無需關(guān)心另外三個服務(wù)是否可用。這時候交易服務(wù)跟另外三個服務(wù)就是弱耦合的關(guān)系,耦合性被降低了。

哪怕是另外三個服務(wù)暫時不可用,也不影響交易服務(wù)的運行,只要其他服務(wù)運行起來后,把MQ里面的消息消費了就行。(降級接口)


image.png
1.1.2 解耦

假設(shè) A 系統(tǒng)在用戶發(fā)生某個操作的時候,需要把用戶提交的數(shù)據(jù)同時推送到 B、C 兩個系統(tǒng)的時候。這個時候負(fù)責(zé) A 系統(tǒng)的哥們想:沒事啊,B、C 兩個系統(tǒng)給我提供一個 HTTP 接口或者 RPC 接口,我把數(shù)據(jù)推送過去不就完事了嘛,負(fù)責(zé) A 系統(tǒng)的哥們美滋滋。

一切看起來很美好,但是隨著業(yè)務(wù)快速迭代,這個時候系統(tǒng) D 也想要這個數(shù)據(jù)。那既然這樣,A 系統(tǒng)的開發(fā)同學(xué)就改咯,在發(fā)送數(shù)據(jù)給 B、C 的同時加上一個 D。但是,越到后面越發(fā)現(xiàn),麻煩來了。整個系統(tǒng)好像不止這個數(shù)據(jù)要發(fā)送給 B、C、D、還有第二、第三個數(shù)據(jù)要發(fā)送給 B、C、D。甚至有時候又加入了 E、F 等系統(tǒng),他們也要這個數(shù)據(jù)。并且有時候可能 B 系統(tǒng)突然又不要這個數(shù)據(jù)了,A 系統(tǒng)改來改去,A 系統(tǒng)的開發(fā)哥們頭皮發(fā)麻。更復(fù)雜的場景是,數(shù)據(jù)通過接口傳給其他系統(tǒng)有時候還要考慮重試、超時等一些異常情況。

這個時候,就該我們的 MQ 粉墨登場了,這種情況下使用 MQ 來解耦是再合適不過了,因為負(fù)責(zé) A 系統(tǒng)的哥們只需要把消息扔到 MQ 就行了,其他系統(tǒng)按需來訂閱消息就好了。就算某個系統(tǒng)不需要這個數(shù)據(jù)了,也不會需要 A 系統(tǒng)改動代碼。

1.2 異步

沒有引入MQ的時候,交易服務(wù)需要同步調(diào)用三個服務(wù),如果調(diào)用一個服務(wù)需要耗時1秒,那么同步調(diào)用三個服務(wù)需要耗時3秒。在引入MQ之后,全都改成了異步調(diào)用,整個耗時不到1秒,大大提高了接口的性能。

1.3 削峰

如果一秒內(nèi)同時來了5000筆交易,而訂單服務(wù)每秒只能處理100筆交易,那么后面的4900筆交易失敗。在引入MQ之后,交易服務(wù)可以把交易數(shù)據(jù)先發(fā)送到MQ中,而訂單服務(wù)再慢慢從MQ拉取交易信息處理。從而避免突發(fā)流量壓垮服務(wù)器。

1.3.1 削峰填谷

舉個例子,比如我們的訂單系統(tǒng),在下單的時候就會往數(shù)據(jù)庫寫數(shù)據(jù)。但是數(shù)據(jù)庫只能支撐每秒 1000 左右的并發(fā)寫入,并發(fā)量再高就容易宕機。低峰期的時候并發(fā)也就 100 多個,但是在高峰期時候,并發(fā)量會突然激增到 5000 以上,這個時候數(shù)據(jù)庫肯定死了。

但是使用了 MQ 之后,情況就變了,消息被 MQ 保存起來了,然后系統(tǒng)就可以按照自己的消費能力來消費,比如每秒 1000 個數(shù)據(jù),這樣慢慢寫入數(shù)據(jù)庫,這樣就不會打死數(shù)據(jù)庫了。

至于為什么叫做削峰填谷呢?如果沒有用 MQ 的情況下,并發(fā)量高峰期的時候是有一個“頂峰”的,然后高峰期過后又是一個低并發(fā)的“谷”。但是使用了 MQ 之后,限制消費消息的速度為 1000QPS,但是這樣一來,高峰期產(chǎn)生的數(shù)據(jù)勢必會被積壓在 MQ 中,高峰就被“削”掉了。但是因為消息積壓,在高峰期過后的一段時間內(nèi),消費消息的速度還是會維持在 1000QPS,直到消費完積壓的消息,這就叫做“填谷”。

二、引入MQ之后的問題

2.1 系統(tǒng)可用性降低

本來整個系統(tǒng)有四個服務(wù),我們只需要保證這四個服務(wù)可用就行了。現(xiàn)在又多引入了一個MQ,我們還要保證MQ的可用,所以整個系統(tǒng)的可用性降低。

2.2 系統(tǒng)復(fù)雜性提高

本來交易服務(wù)是同步調(diào)用另外三個服務(wù),如果另外三個服務(wù)不可用,交易服務(wù)能立即感知到。引入MQ之后,整個系統(tǒng)的穩(wěn)定性就要靠MQ保證了。

這時候,我們就要考慮到發(fā)到MQ里面的消息怎么避免丟失的問題?順序性消費的問題,就是同一筆交易的下單消息應(yīng)該比撤單消息先處理。重復(fù)性消費的問題,就是同一筆下單交易的消息可能被多次處理。

當(dāng)然,每種問題都有具體的解決方案,避免消息丟失可以使用MQ集群,順序性消費可以把消息發(fā)到同一個分區(qū),重復(fù)性消費可以在消費端做冪等性處理。

2.3 重復(fù)消費問題

2.3.1 問題場景

重復(fù)消費問題可以說是 MQ 中普遍存在的問題, 不管你用哪種 MQ 都無法避免。有哪些場景會出現(xiàn)重復(fù)的消息呢?

  • 消息生產(chǎn)者產(chǎn)生了重復(fù)的消息;
  • Kafka 和 RocketMQ 的 offset 被回調(diào)了;
  • 消息消費者確認(rèn)失??;
  • 消息消費者確認(rèn)時超時;
  • 業(yè)務(wù)系統(tǒng)主動發(fā)起重試。

如果重復(fù)消息不做正確的處理,會對業(yè)務(wù)造成很大的影響,產(chǎn)生重復(fù)數(shù)據(jù)或者導(dǎo)致數(shù)據(jù)異常,比如會員系統(tǒng)多開通了一個月的會員等。

2.3.2 解決方案

不管是由于生產(chǎn)者產(chǎn)生的重復(fù)消息,還是由于消費者導(dǎo)致的重復(fù)消息,我們都可以在消費者中解決這個問題。

這就要求消費者在做業(yè)務(wù)處理時,要做冪等設(shè)計。在這里我推薦增加一張消費消息表,來解決 MQ的這類問題。

消費消息表中,使用 messageId 做唯一索引。在處理業(yè)務(wù)邏輯之前,先根據(jù) messageId 查詢一下該消息有沒有處理過。如果已經(jīng)處理過了則直接返回成功,如果沒有處理過,則繼續(xù)做業(yè)務(wù)處理。


image.png

2.4 數(shù)據(jù)一致性問題(異步分布式事務(wù)問題)

2.4.1 問題場景

當(dāng)服務(wù)間是同步調(diào)用的時候,我們還可以使用本地事務(wù)來控制數(shù)據(jù)的一致性。但是引入MQ之后,服務(wù)間的調(diào)用都是異步了,就沒辦法使用本地事務(wù),也就無法做到數(shù)據(jù)的強一致性了。

例如,調(diào)用訂單服務(wù)下單成功了,但是調(diào)用庫存服務(wù)扣減庫存失敗,就會導(dǎo)致超賣,是嚴(yán)重的線上事故。

這時候怎么辦?
方案一:需要事務(wù)強一致的,不用消息異步,如下單、減庫存要放在一個事務(wù)里控制,加積分這種非核心的業(yè)務(wù)才用消息異步處理。

image.png

方案二:可以使用MQ事務(wù)消息(只有RocketMQ才有事務(wù)消息功能,RocketMQ收發(fā)事務(wù)消息)。

事務(wù)狀態(tài)有以下三種:

  • TransactionStatus.CommitTransaction:提交事務(wù),允許訂閱方消費該消息。
    TransactionStatus.RollbackTransaction:回滾事務(wù),消息將被丟棄不允許消費。
    TransactionStatus.Unknow:無法判斷狀態(tài),期待消息隊列RocketMQ版的Broker向發(fā)送方再次詢問該消息對應(yīng)的本地事務(wù)的狀態(tài)。
步驟一: A 服務(wù)向消息中間件發(fā)布消息
  • 在服務(wù)A處理任務(wù)A前,首先向消息中間件發(fā)送一條半信息。
  • 消息中間件收到后將該消息持久化,但不進(jìn)行投遞。持久化成功后,向A服務(wù)返回確認(rèn)應(yīng)答。
  • 服務(wù)A收到確認(rèn)應(yīng)答后,便可以開始處理任務(wù)A。
  • 任務(wù)A處理完成后,服務(wù)A便會向消息中間件發(fā)送Commit 或者 Rollback 請求,該請求發(fā)送完成后,服務(wù)A的工作任務(wù)就結(jié)束了,該事務(wù)的處理過程也就結(jié)束了。
  • 在消息中間件收到 Commit 后,便會向 B 服務(wù)投遞消息,如果收到 Rollback 便會直接丟棄消息。

如果消息中間件在最后的過程中,長時間沒有收到服務(wù)A 發(fā)送的 Commit 或 Rollback 指令,這個時候就需要依靠 超時詢問機制。

步驟二: 消息中間件向B服務(wù)投遞消息

消息中間件收到A服務(wù)的提交 Commit指令后便會將該消息投遞給B服務(wù),然后將自己的狀態(tài)置為阻塞等待狀態(tài)。B服務(wù)收到消息中間件發(fā)送的消息后便開始處理任務(wù)B,處理完成后便會向消息中間件發(fā)出回應(yīng)。但是在消息中間件阻塞等待的時候同樣會出現(xiàn)問題。

正常情況:消息中間件投遞完消息后,進(jìn)入阻塞等待狀態(tài),在收到確認(rèn)應(yīng)答后便認(rèn)為事務(wù)處理完成,該流程結(jié)束。
等待超時情況:在等待確認(rèn)應(yīng)答超時之后就會重新進(jìn)行投遞,直到B服務(wù)器返回消費成功響應(yīng)為止。而消息重試的次數(shù)和時間間隔都可以設(shè)置,如果最終還是不能成功進(jìn)行投遞,則需要人工干預(yù)。

2.4.2 解決方案

我們都知道數(shù)據(jù)一致性分為:強一致性、弱一致性、最終一致性。

而 MQ 為了性能考慮使用的是最終一致性,那么必定會出現(xiàn)數(shù)據(jù)不一致的問題。這類問題大概率是因為消費者讀取消息后,業(yè)務(wù)邏輯處理失敗導(dǎo)致的。這時候可以增加重試機制。重試分為同步重試和異步重試。

有些消息量比較小的業(yè)務(wù)場景,可以采用同步重試。在消費消息時如果處理失敗,立刻重試 3-5 次,如果還是失敗則寫入到記錄表中。但如果消息量比較大,則不建議使用這種方式。因為如果出現(xiàn)網(wǎng)絡(luò)異常,可能會導(dǎo)致大量的消息不斷重試,影響消息讀取速度造成消息堆積。


image.png

消息量比較大的業(yè)務(wù)場景,建議采用異步重試。在消費者處理失敗之后,立刻寫入重試表,有個 job(如采用xxljob) 專門定時重試。

還有一種做法:如果消費失敗,自己給同一個 topic 發(fā)一條消息。在后面的某個時間點,自己又會消費到那條消息,起到了重試的效果。如果對消息順序要求不高的場景,可以使用這種方式。

2.5 消息丟失問題

2.5.1 問題場景

同樣消息丟失問題,也是 MQ 中普遍存在的問題,不管你用哪種 MQ 都無法避免。有哪些場景會出現(xiàn)消息丟失問題呢?

  • 生產(chǎn)者產(chǎn)生消息時,由于網(wǎng)絡(luò)原因發(fā)送到 MQ 失敗了;
  • MQ 服務(wù)器持久化,存儲磁盤時出現(xiàn)異常;
  • Kafka和RocketMQ 的 offset 被回調(diào)時,略過了很多消息;
  • 消費者剛讀取消息,已經(jīng) ACK 確認(rèn),但業(yè)務(wù)還沒處理完,服務(wù)就被重啟了。

導(dǎo)致消息丟失問題的原因挺多的, 生產(chǎn)者、 MQ 服務(wù)器、 消費者都有可能產(chǎn)生問題。我在這里就不一一列舉了。最終的結(jié)果會導(dǎo)致消費者無法正確的處理消息,而導(dǎo)致數(shù)據(jù)不一致的情況。

2.5.2 解決方案

不管你是否承認(rèn),有時候消息真的會丟。即使這種概率非常小,也會對業(yè)務(wù)有影響。生產(chǎn)者、MQ 服務(wù)器、消費者都有可能會導(dǎo)致消息丟失的問題。為了解決這個問題,我們可以增加一張消息發(fā)送表。

  • 當(dāng)生產(chǎn)者發(fā)完消息之后,會往該表中寫入一條數(shù)據(jù),狀態(tài) status 標(biāo)記為待確認(rèn);

  • 如果消費者讀取消息之后,調(diào)用生產(chǎn)者的 API 更新該消息的status為已確認(rèn);

  • 有個job(xxljob) 每隔一段時間檢查一次消息發(fā)送表,如果5分鐘(這個時間可以根據(jù)實際情況來定)后還有狀態(tài)是待確認(rèn)的消息,則認(rèn)為該消息已經(jīng)丟失了,重新發(fā)條消息。


    image.png

    這樣不管是由于生產(chǎn)者、 MQ服務(wù)器、還是消費者導(dǎo)致的消息丟失問題,job 都會重新發(fā)消息。

2.6 消息順序問題

2.6.1 問題場景

有些業(yè)務(wù)數(shù)據(jù)是有狀態(tài)的,比如訂單有下單、支付、完成、退貨等狀態(tài)。 如果訂單數(shù)據(jù)作為消息體,就會涉及順序問題了。

例如消費者收到同一個訂單的兩條消息。第一條消息的狀態(tài)是下單,第二條消息的狀態(tài)是支付,這是沒問題的。但如果第一條消息的狀態(tài)是支付,第二條消息的狀態(tài)是下單就會有問題了。沒有下單就先支付了?


image.png

消息順序問題是一個非常棘手的問題,比如:

  • Kafka 同一個 partition 中能保證順序,但是不同的 partition 無法保證順序;

  • RabbitMQ的同一個queue能夠保證順序,但是如果多個消費者同一個queue 也會有順序問題。

  • 如果消費者使用多線程消費消息,也無法保證順序。

  • 如果消費消息時同一個訂單的多條消息中,中間的一條消息出現(xiàn)異常情況,順序?qū)淮騺y。

  • 還有如果生產(chǎn)者發(fā)送到 MQ中的路由規(guī)則,跟消費者不一樣,也無法保證順序。

2.6.2 解決方案

消息順序問題是一種常見問題。我們以 Kafka 消費訂單消息為例,訂單有下單、 支付、 完成、 退貨等狀態(tài)。這些狀態(tài)是有先后順序的,如果順序錯了會導(dǎo)致業(yè)務(wù)異常。

解決這類問題之前,我們需要先確認(rèn):消費者是否真的需要知道中間狀態(tài),只知道最終狀態(tài)行不行?


image.png

其實很多時候,我真的需要知道的是最終狀態(tài)。這時可以把流程優(yōu)化一下:


image.png

這種方式可以解決大部分的消息順序問題。

但如果真的有需要保證消息順序的需求,那么可以將訂單號路由到不同的 partition。同一個訂單號的消息,每次到發(fā)到同一個partition。
2.7 消息堆積
2.7.1 問題場景
如果消息消費者讀取消息的速度,能夠跟上消息生產(chǎn)者的節(jié)奏,那么整套 MQ 機制就能發(fā)揮最大作用。

但是很多時候,由于某些批處理或者其他原因,導(dǎo)致消費速度小于生產(chǎn)速度。這樣會直接導(dǎo)致消息堆積問題,從而影響業(yè)務(wù)功能。

這里以下單 開通會員為例,如果消息出現(xiàn)堆積會導(dǎo)致用戶下單之后,很久之后才能變成會員。這種情況肯定會引起大量用戶投訴。

2.7.2 解決方案
那么消息堆積問題該如何解決呢?這個要看消息是否需要保證順序。如果不需要保證順序,可以讀取消息之后用多線程處理業(yè)務(wù)邏輯。


image.png

這樣就能增加業(yè)務(wù)邏輯處理速度,解決消息堆積問題。但是線程池的核心線程數(shù)和最大線程數(shù)需要合理配置,不然可能會浪費系統(tǒng)資源。

如果需要保證順序,可以讀取消息之后將消息按照一定的規(guī)則分發(fā)到多個隊列中,然后在隊列中用單線程處理。

image.png

資料來源:
面試官竟然問我為啥要用MQ,幸虧我看了參考答案

面霸篇:MQ 的 5 大問題詳解

消息中間件學(xué)習(xí)總結(jié)(18)——MQ常見面試題總結(jié)

作者:陳琰AC
鏈接:http://m.itdecent.cn/p/439f74dd62a9

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容