前言
最近,消息隊(duì)列(Message Queue ,簡稱 MQ)越來越火。很多公司在用,很多人在用,其重要性不言而喻。
如果讓你回答下面這些問題,你的心中是否有答案了呢?
為什么要用 MQ?
引入 MQ 會多出哪些問題?
如何解決這些問題?
本文將會一一為你解答,這些看似平常卻很有意義的問題。
1. 傳統(tǒng)模式有哪些痛點(diǎn)
痛點(diǎn) 1
有些復(fù)雜的業(yè)務(wù)系統(tǒng),一次用戶請求可能會同步調(diào)用 N 個(gè)系統(tǒng)的接口,需要等待所有的接口都返回才能真正的獲取執(zhí)行結(jié)果。
這種同步接口調(diào)用的方式總耗時(shí)比較長,非常影響用戶體驗(yàn)。特別是在網(wǎng)絡(luò)不穩(wěn)定的情況下,極容易出現(xiàn)接口超時(shí)問題。
痛點(diǎn) 2
很多復(fù)雜的業(yè)務(wù)系統(tǒng),一般都會拆分成多個(gè)子系統(tǒng)。以用戶下單為例,請求會先通過訂單系統(tǒng),然后分別調(diào)用支付系統(tǒng)、庫存系統(tǒng)、積分系統(tǒng)和物流系統(tǒng)。
系統(tǒng)之間耦合性太高,如果調(diào)用的任何一個(gè)子系統(tǒng)出現(xiàn)異常,整個(gè)請求都會異常。對系統(tǒng)的穩(wěn)定性非常不利。
痛點(diǎn) 3
為了吸引用戶,有時(shí)候會搞一些活動,比如秒殺等。
如果用戶少還好,不會影響系統(tǒng)的穩(wěn)定性。但如果用戶突增,一時(shí)間所有的請求都到數(shù)據(jù)庫,可能會導(dǎo)致數(shù)據(jù)庫無法承受這么大的壓力,響應(yīng)變慢或者直接掛掉。
對于這種突然出現(xiàn)的請求峰值,無法保證系統(tǒng)的穩(wěn)定性。
2. 為什么要用 MQ
對于上面?zhèn)鹘y(tǒng)模式的三類問題,使用 MQ 就能輕松解決。
2.1 異步
對于痛點(diǎn) 1 同步接口調(diào)用導(dǎo)致響應(yīng)時(shí)間長的問題。使用 MQ 之后,將同步調(diào)用改成異步調(diào)用,能夠顯著減少系統(tǒng)響應(yīng)時(shí)間。
系統(tǒng) A 作為消息的生產(chǎn)者,在完成本職工作后就能直接返回結(jié)果了。無需等待消息消費(fèi)者的返回,它們最終會獨(dú)立完成所有的業(yè)務(wù)功能。
這樣能避免總耗時(shí)比較長,從而影響用戶的體驗(yàn)的問題。
2.2 解耦
對于痛點(diǎn) 2 子系統(tǒng)間耦合性太大的問題,使用 MQ 之后,只需要依賴于 MQ。避免了各個(gè)子系統(tǒng)間的強(qiáng)依賴問題。
訂單系統(tǒng)作為消息生產(chǎn)者,保證它自己沒有異常即可,不會受到支付系統(tǒng)等業(yè)務(wù)子系統(tǒng)的異常影響。并且各個(gè)消費(fèi)者業(yè)務(wù)子系統(tǒng)之間,也互不影響。
這樣就把之前復(fù)雜的業(yè)務(wù)子系統(tǒng)的依賴關(guān)系,轉(zhuǎn)換為只依賴于 MQ 的簡單依賴,從而顯著的降低了系統(tǒng)間的耦合度。
2.3 削峰
對于痛點(diǎn) 3,由于突然出現(xiàn)的請求峰值導(dǎo)致系統(tǒng)不穩(wěn)定的問題。使用 MQ 后,能夠起到削峰的作用。
訂單系統(tǒng)接收到用戶請求之后,將請求直接發(fā)送到 MQ;
然后,訂單消費(fèi)者從 MQ 中消費(fèi)消息,做寫庫操作;
當(dāng)出現(xiàn)請求峰值的情況,由于消費(fèi)者的消費(fèi)能力有限,會按照自己的節(jié)奏來消費(fèi)消息。多余請求不處理,保留在 MQ 的隊(duì)列中,不會對系統(tǒng)的穩(wěn)定性造成影響。
3. 引入 MQ 會多出哪些問題
引入 MQ 后讓子系統(tǒng)間耦合性降低了,異步處理機(jī)制減少了系統(tǒng)的響應(yīng)時(shí)間。同時(shí)能夠有效的應(yīng)對請求峰值問題,提升了系統(tǒng)的穩(wěn)定性。
但是,引入 MQ 的同時(shí)也會帶來一些問題。
3.1 重復(fù)消費(fèi)問題
重復(fù)消費(fèi)問題可以說是 MQ 中普遍存在的問題,不管你用哪種 MQ 都無法避免。
有哪些場景會出現(xiàn)重復(fù)的消息呢?
消息生產(chǎn)者產(chǎn)生了重復(fù)的消息;
Kafka 和 RocketMQ 的 offset 被回調(diào)了;
消息消費(fèi)者確認(rèn)失??;
消息消費(fèi)者確認(rèn)時(shí)超時(shí);
業(yè)務(wù)系統(tǒng)主動發(fā)起重試。
如果重復(fù)消息不做正確的處理,會對業(yè)務(wù)造成很大的影響,產(chǎn)生重復(fù)數(shù)據(jù)或者導(dǎo)致數(shù)據(jù)異常,比如會員系統(tǒng)多開通了一個(gè)月的會員等。
3.2 數(shù)據(jù)一致性問題
很多時(shí)候,如果 MQ 的消費(fèi)者業(yè)務(wù)處理異常,就會出現(xiàn)數(shù)據(jù)一致性問題。
舉個(gè)例子,一個(gè)完整的業(yè)務(wù)流程是,下單成功之后送 100 個(gè)積分。下單寫庫成功了,但是消息消費(fèi)者在送積分的時(shí)候失敗了。這樣就會造成數(shù)據(jù)不一致的情況,即該業(yè)務(wù)流程的部分?jǐn)?shù)據(jù)寫庫了,另外一部分沒有寫庫。
如果下單和送積分在同一個(gè)事務(wù)中,要么同時(shí)成功,要么同時(shí)失敗。這樣不會出現(xiàn)數(shù)據(jù)一致性問題的。
但由于跨系統(tǒng)調(diào)用,為了性能考慮一般不會使用強(qiáng)一致性的方案,而改成達(dá)成最終一致性即可。
3.3 消息丟失問題
同樣消息丟失問題,也是 MQ 中普遍存在的問題,不管你用哪種 MQ 都無法避免。
有哪些場景會出現(xiàn)消息丟失問題呢?
生產(chǎn)者產(chǎn)生消息時(shí),由于網(wǎng)絡(luò)原因發(fā)送到 MQ 失敗了;
MQ 服務(wù)器持久化,存儲磁盤時(shí)出現(xiàn)異常;
Kafka 和 RocketMQ 的 offset 被回調(diào)時(shí),略過了很多消息;
消費(fèi)者剛讀取消息,已經(jīng) ACK 確認(rèn),但業(yè)務(wù)還沒處理完,服務(wù)就被重啟了。
導(dǎo)致消息丟失問題的原因挺多的,生產(chǎn)者、MQ 服務(wù)器、消費(fèi)者都有可能產(chǎn)生問題。我在這里就不一一列舉了。
最終的結(jié)果會導(dǎo)致消費(fèi)者無法正確的處理消息,而導(dǎo)致數(shù)據(jù)不一致的情況。
3.4 消息順序問題
有些業(yè)務(wù)數(shù)據(jù)是有狀態(tài)的,比如訂單有下單、支付、完成、退貨等狀態(tài)。如果訂單數(shù)據(jù)作為消息體,就會涉及順序問題了。
例如消費(fèi)者收到同一個(gè)訂單的兩條消息。第一條消息的狀態(tài)是下單,第二條消息的狀態(tài)是支付,這是沒問題的。
但如果第一條消息的狀態(tài)是支付,第二條消息的狀態(tài)是下單就會有問題了。沒有下單就先支付了?
消息順序問題是一個(gè)非常棘手的問題,比如:
Kafka 同一個(gè) partition 中能保證順序,但是不同的 partition 無法保證順序;
RabbitMQ的同一個(gè) queue 能夠保證順序,但是如果多個(gè)消費(fèi)者同一個(gè) queue 也會有順序問題。
如果消費(fèi)者使用多線程消費(fèi)消息,也無法保證順序。
如果消費(fèi)消息時(shí)同一個(gè)訂單的多條消息中,中間的一條消息出現(xiàn)異常情況,順序?qū)淮騺y。
還有如果生產(chǎn)者發(fā)送到 MQ 中的路由規(guī)則,跟消費(fèi)者不一樣,也無法保證順序。
3.5 消息堆積
如果消息消費(fèi)者讀取消息的速度,能夠跟上消息生產(chǎn)者的節(jié)奏,那么整套 MQ 機(jī)制就能發(fā)揮最大作用。
但是很多時(shí)候,由于某些批處理或者其他原因,導(dǎo)致消費(fèi)速度小于生產(chǎn)速度。這樣會直接導(dǎo)致消息堆積問題,從而影響業(yè)務(wù)功能。
這里以下單開通會員為例,如果消息出現(xiàn)堆積會導(dǎo)致用戶下單之后,很久之后才能變成會員。這種情況肯定會引起大量用戶投訴。
3.6 系統(tǒng)復(fù)雜度提升
這里說的系統(tǒng)復(fù)雜度和系統(tǒng)耦合性是不一樣的。
假設(shè)以前只有系統(tǒng) A、系統(tǒng) B 和系統(tǒng) C 三個(gè)系統(tǒng),引入 MQ 之后,除了需要關(guān)注前面三個(gè)系統(tǒng)之外,還需要關(guān)注 MQ 服務(wù)。需要關(guān)注的點(diǎn)越多,系統(tǒng)的復(fù)雜度越高。
MQ 的機(jī)制需要生產(chǎn)者、MQ 服務(wù)器、消費(fèi)者。有一定的學(xué)習(xí)成本,需要額外部署 MQ 服務(wù)器。
有些 MQ 功能非常強(qiáng)大、用法有點(diǎn)復(fù)雜,例如 RocketMQ。如果使用不好,會出現(xiàn)很多問題。有些問題,不像接口調(diào)用那么容易排查,從而導(dǎo)致系統(tǒng)的復(fù)雜度提升了。
4. 如何解決這些問題?
MQ 是一種趨勢,總體來說對我們的系統(tǒng)是利大于弊的,難道因?yàn)樗鼤霈F(xiàn)一些問題,我們就不用它了?那么我們要如何解決這些問題呢?
4.1 重復(fù)消息問題
不管是由于生產(chǎn)者產(chǎn)生的重復(fù)消息,還是由于消費(fèi)者導(dǎo)致的重復(fù)消息,我們都可以在消費(fèi)者中解決這個(gè)問題。
這就要求消費(fèi)者在做業(yè)務(wù)處理時(shí),要做冪等設(shè)計(jì)。如果有不知道如何設(shè)計(jì)的朋友,可以參考“高并發(fā)下如何保證接口的冪等性?”,里面介紹得非常詳細(xì)。
在這里我推薦增加一張消費(fèi)消息表,來解決 MQ 的這類問題。
消費(fèi)消息表中,使用 messageId 做唯一索引。在處理業(yè)務(wù)邏輯之前,先根據(jù) messageId 查詢一下該消息有沒有處理過。如果已經(jīng)處理過了則直接返回成功,如果沒有處理過,則繼續(xù)做業(yè)務(wù)處理。
4.2 數(shù)據(jù)一致性問題
我們都知道數(shù)據(jù)一致性分為:
強(qiáng)一致性
弱一致性
最終一致性
而 MQ 為了性能考慮使用的是最終一致性,那么必定會出現(xiàn)數(shù)據(jù)不一致的問題。這類問題大概率是因?yàn)橄M(fèi)者讀取消息后,業(yè)務(wù)邏輯處理失敗導(dǎo)致的。這時(shí)候可以增加重試機(jī)制。
重試分為同步重試和異步重試。
有些消息量比較小的業(yè)務(wù)場景,可以采用同步重試。在消費(fèi)消息時(shí)如果處理失敗,立刻重試 3-5 次,如果還是失敗則寫入到記錄表中。
但如果消息量比較大,則不建議使用這種方式。因?yàn)槿绻霈F(xiàn)網(wǎng)絡(luò)異常,可能會導(dǎo)致大量的消息不斷重試,影響消息讀取速度造成消息堆積。
消息量比較大的業(yè)務(wù)場景,建議采用異步重試。在消費(fèi)者處理失敗之后,立刻寫入重試表,有個(gè) job 專門定時(shí)重試。
還有一種做法:如果消費(fèi)失敗,自己給同一個(gè) topic 發(fā)一條消息。在后面的某個(gè)時(shí)間點(diǎn),自己又會消費(fèi)到那條消息,起到了重試的效果。如果對消息順序要求不高的場景,可以使用這種方式。
4.3 消息丟失問題
不管你是否承認(rèn),有時(shí)候消息真的會丟。即使這種概率非常小,也會對業(yè)務(wù)有影響。生產(chǎn)者、MQ 服務(wù)器、消費(fèi)者都有可能會導(dǎo)致消息丟失的問題。為了解決這個(gè)問題,我們可以增加一張消息發(fā)送表。
當(dāng)生產(chǎn)者發(fā)完消息之后,會往該表中寫入一條數(shù)據(jù),狀態(tài) status 標(biāo)記為待確認(rèn);
如果消費(fèi)者讀取消息之后,調(diào)用生產(chǎn)者的 API 更新該消息的 status 為已確認(rèn);
有個(gè) job 每隔一段時(shí)間檢查一次消息發(fā)送表,如果5分鐘(這個(gè)時(shí)間可以根據(jù)實(shí)際情況來定)后還有狀態(tài)是待確認(rèn)的消息,則認(rèn)為該消息已經(jīng)丟失了,重新發(fā)條消息。
這樣不管是由于生產(chǎn)者、MQ 服務(wù)器、還是消費(fèi)者導(dǎo)致的消息丟失問題,job 都會重新發(fā)消息。
4.4 消息順序問題
消息順序問題是一種常見問題。我們以 Kafka 消費(fèi)訂單消息為例,訂單有下單、支付、完成、退貨等狀態(tài)。這些狀態(tài)是有先后順序的,如果順序錯(cuò)了會導(dǎo)致業(yè)務(wù)異常。
解決這類問題之前,我們需要先確認(rèn):消費(fèi)者是否真的需要知道中間狀態(tài),只知道最終狀態(tài)行不行?
其實(shí)很多時(shí)候,我真的需要知道的是最終狀態(tài)。這時(shí)可以把流程優(yōu)化一下:
這種方式可以解決大部分的消息順序問題。
但如果真的有需要保證消息順序的需求,那么可以將訂單號路由到不同的 partition。同一個(gè)訂單號的消息,每次到發(fā)到同一個(gè) partition。
4.5 消息堆積
如果消費(fèi)者消費(fèi)消息的速度小于生產(chǎn)者生產(chǎn)消息的速度,將會出現(xiàn)消息堆積問題。其實(shí)這類問題產(chǎn)生的原因很多。如果想要進(jìn)一步了解,可以看看“我用kafka兩年踩過的一些非比尋常的坑”。
那么消息堆積問題該如何解決呢?這個(gè)要看消息是否需要保證順序。
如果不需要保證順序,可以讀取消息之后用多線程處理業(yè)務(wù)邏輯。
這樣就能增加業(yè)務(wù)邏輯處理速度,解決消息堆積問題。但是線程池的核心線程數(shù)和最大線程數(shù)需要合理配置,不然可能會浪費(fèi)系統(tǒng)資源。
如果需要保證順序,可以讀取消息之后將消息按照一定的規(guī)則分發(fā)到多個(gè)隊(duì)列中,然后在隊(duì)列中用單線程處理。
責(zé)任編輯:haq
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9021瀏覽量
85184 -
消息
+關(guān)注
關(guān)注
0文章
29瀏覽量
12867
原文標(biāo)題:面霸篇:MQ 的 5 大問題詳解
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論