一逝她、為什么使用 MQ?
1.1 解耦
1.1.1 解耦
例如電商系統(tǒng)核心是交易服務,交易服務要調用另外三個服務睬捶,訂單服務黔宛、庫存服務、倉儲服務擒贸。
這三個服務如果有一個服務不可用臀晃,交易服務就無法正常運行,所以交易服務是強耦合另外三個服務介劫。
引入MQ之后徽惋,交易服務只跟MQ交互,把消息發(fā)到MQ里面就行了座韵,無需關心另外三個服務是否可用险绘。這時候交易服務跟另外三個服務就是弱耦合的關系踢京,耦合性被降低了。
哪怕是另外三個服務暫時不可用宦棺,也不影響交易服務的運行瓣距,只要其他服務運行起來后,把MQ里面的消息消費了就行代咸。(降級接口)
1.1.2 解耦
假設 A 系統(tǒng)在用戶發(fā)生某個操作的時候蹈丸,需要把用戶提交的數據同時推送到 B、C 兩個系統(tǒng)的時候呐芥。這個時候負責 A 系統(tǒng)的哥們想:沒事啊逻杖,B、C 兩個系統(tǒng)給我提供一個 HTTP 接口或者 RPC 接口思瘟,我把數據推送過去不就完事了嘛荸百,負責 A 系統(tǒng)的哥們美滋滋。
一切看起來很美好潮太,但是隨著業(yè)務快速迭代管搪,這個時候系統(tǒng) D 也想要這個數據。那既然這樣铡买,A 系統(tǒng)的開發(fā)同學就改咯,在發(fā)送數據給 B霎箍、C 的同時加上一個 D奇钞。但是,越到后面越發(fā)現漂坏,麻煩來了景埃。整個系統(tǒng)好像不止這個數據要發(fā)送給 B、C顶别、D谷徙、還有第二、第三個數據要發(fā)送給 B驯绎、C完慧、D。甚至有時候又加入了 E剩失、F 等系統(tǒng)屈尼,他們也要這個數據。并且有時候可能 B 系統(tǒng)突然又不要這個數據了拴孤,A 系統(tǒng)改來改去脾歧,A 系統(tǒng)的開發(fā)哥們頭皮發(fā)麻。更復雜的場景是演熟,數據通過接口傳給其他系統(tǒng)有時候還要考慮重試鞭执、超時等一些異常情況。
這個時候,就該我們的 MQ 粉墨登場了兄纺,這種情況下使用 MQ 來解耦是再合適不過了大溜,因為負責 A 系統(tǒng)的哥們只需要把消息扔到 MQ 就行了,其他系統(tǒng)按需來訂閱消息就好了囤热。就算某個系統(tǒng)不需要這個數據了猎提,也不會需要 A 系統(tǒng)改動代碼。
1.2 異步
沒有引入MQ的時候旁蔼,交易服務需要同步調用三個服務锨苏,如果調用一個服務需要耗時1秒,那么同步調用三個服務需要耗時3秒棺聊。在引入MQ之后伞租,全都改成了異步調用,整個耗時不到1秒限佩,大大提高了接口的性能葵诈。
1.3 削峰
如果一秒內同時來了5000筆交易,而訂單服務每秒只能處理100筆交易祟同,那么后面的4900筆交易失敗作喘。在引入MQ之后,交易服務可以把交易數據先發(fā)送到MQ中晕城,而訂單服務再慢慢從MQ拉取交易信息處理泞坦。從而避免突發(fā)流量壓垮服務器。
1.3.1 削峰填谷
舉個例子砖顷,比如我們的訂單系統(tǒng)贰锁,在下單的時候就會往數據庫寫數據。但是數據庫只能支撐每秒 1000 左右的并發(fā)寫入滤蝠,并發(fā)量再高就容易宕機豌熄。低峰期的時候并發(fā)也就 100 多個,但是在高峰期時候物咳,并發(fā)量會突然激增到 5000 以上锣险,這個時候數據庫肯定死了。
但是使用了 MQ 之后所森,情況就變了囱持,消息被 MQ 保存起來了,然后系統(tǒng)就可以按照自己的消費能力來消費焕济,比如每秒 1000 個數據纷妆,這樣慢慢寫入數據庫,這樣就不會打死數據庫了晴弃。
至于為什么叫做削峰填谷呢掩幢?如果沒有用 MQ 的情況下逊拍,并發(fā)量高峰期的時候是有一個“頂峰”的,然后高峰期過后又是一個低并發(fā)的“谷”际邻。但是使用了 MQ 之后芯丧,限制消費消息的速度為 1000QPS,但是這樣一來世曾,高峰期產生的數據勢必會被積壓在 MQ 中缨恒,高峰就被“削”掉了。但是因為消息積壓轮听,在高峰期過后的一段時間內骗露,消費消息的速度還是會維持在 1000QPS,直到消費完積壓的消息血巍,這就叫做“填谷”萧锉。
二、引入MQ之后的問題
2.1 系統(tǒng)可用性降低
本來整個系統(tǒng)有四個服務述寡,我們只需要保證這四個服務可用就行了∈料叮現在又多引入了一個MQ,我們還要保證MQ的可用鲫凶,所以整個系統(tǒng)的可用性降低禀崖。
2.2 系統(tǒng)復雜性提高
本來交易服務是同步調用另外三個服務,如果另外三個服務不可用螟炫,交易服務能立即感知到帆焕。引入MQ之后,整個系統(tǒng)的穩(wěn)定性就要靠MQ保證了不恭。
這時候,我們就要考慮到發(fā)到MQ里面的消息怎么避免丟失的問題财饥?順序性消費的問題换吧,就是同一筆交易的下單消息應該比撤單消息先處理。重復性消費的問題钥星,就是同一筆下單交易的消息可能被多次處理沾瓦。
當然,每種問題都有具體的解決方案谦炒,避免消息丟失可以使用MQ集群贯莺,順序性消費可以把消息發(fā)到同一個分區(qū),重復性消費可以在消費端做冪等性處理宁改。
2.3 重復消費問題
2.3.1 問題場景
重復消費問題可以說是 MQ 中普遍存在的問題缕探, 不管你用哪種 MQ 都無法避免。有哪些場景會出現重復的消息呢还蹲?
- 消息生產者產生了重復的消息爹耗;
- Kafka 和 RocketMQ 的 offset 被回調了耙考;
- 消息消費者確認失敗潭兽;
- 消息消費者確認時超時倦始;
- 業(yè)務系統(tǒng)主動發(fā)起重試。
如果重復消息不做正確的處理山卦,會對業(yè)務造成很大的影響鞋邑,產生重復數據或者導致數據異常,比如會員系統(tǒng)多開通了一個月的會員等账蓉。
2.3.2 解決方案
不管是由于生產者產生的重復消息枚碗,還是由于消費者導致的重復消息,我們都可以在消費者中解決這個問題剔猿。
這就要求消費者在做業(yè)務處理時视译,要做冪等設計。在這里我推薦增加一張消費消息表归敬,來解決 MQ的這類問題酷含。
消費消息表中,使用 messageId 做唯一索引汪茧。在處理業(yè)務邏輯之前椅亚,先根據 messageId 查詢一下該消息有沒有處理過。如果已經處理過了則直接返回成功舱污,如果沒有處理過呀舔,則繼續(xù)做業(yè)務處理。
2.4 數據一致性問題(異步分布式事務問題)
2.4.1 問題場景
當服務間是同步調用的時候扩灯,我們還可以使用本地事務來控制數據的一致性媚赖。但是引入MQ之后,服務間的調用都是異步了珠插,就沒辦法使用本地事務惧磺,也就無法做到數據的強一致性了。
例如捻撑,調用訂單服務下單成功了磨隘,但是調用庫存服務扣減庫存失敗,就會導致超賣顾患,是嚴重的線上事故番捂。
這時候怎么辦?
方案一:需要事務強一致的江解,不用消息異步设预,如下單、減庫存要放在一個事務里控制膘流,加積分這種非核心的業(yè)務才用消息異步處理絮缅。
方案二:可以使用MQ事務消息(只有RocketMQ才有事務消息功能鲁沥,RocketMQ收發(fā)事務消息)。
事務狀態(tài)有以下三種:
- TransactionStatus.CommitTransaction:提交事務耕魄,允許訂閱方消費該消息画恰。
TransactionStatus.RollbackTransaction:回滾事務,消息將被丟棄不允許消費吸奴。
TransactionStatus.Unknow:無法判斷狀態(tài)允扇,期待消息隊列RocketMQ版的Broker向發(fā)送方再次詢問該消息對應的本地事務的狀態(tài)。
步驟一: A 服務向消息中間件發(fā)布消息
- 在服務A處理任務A前则奥,首先向消息中間件發(fā)送一條半信息考润。
- 消息中間件收到后將該消息持久化,但不進行投遞读处。持久化成功后糊治,向A服務返回確認應答。
- 服務A收到確認應答后罚舱,便可以開始處理任務A井辜。
- 任務A處理完成后,服務A便會向消息中間件發(fā)送Commit 或者 Rollback 請求管闷,該請求發(fā)送完成后粥脚,服務A的工作任務就結束了,該事務的處理過程也就結束了包个。
- 在消息中間件收到 Commit 后刷允,便會向 B 服務投遞消息,如果收到 Rollback 便會直接丟棄消息碧囊。
如果消息中間件在最后的過程中树灶,長時間沒有收到服務A 發(fā)送的 Commit 或 Rollback 指令,這個時候就需要依靠 超時詢問機制糯而。
步驟二: 消息中間件向B服務投遞消息
消息中間件收到A服務的提交 Commit指令后便會將該消息投遞給B服務破托,然后將自己的狀態(tài)置為阻塞等待狀態(tài)。B服務收到消息中間件發(fā)送的消息后便開始處理任務B歧蒋,處理完成后便會向消息中間件發(fā)出回應。但是在消息中間件阻塞等待的時候同樣會出現問題州既。
正常情況:消息中間件投遞完消息后谜洽,進入阻塞等待狀態(tài),在收到確認應答后便認為事務處理完成吴叶,該流程結束阐虚。
等待超時情況:在等待確認應答超時之后就會重新進行投遞,直到B服務器返回消費成功響應為止蚌卤。而消息重試的次數和時間間隔都可以設置实束,如果最終還是不能成功進行投遞奥秆,則需要人工干預。
2.4.2 解決方案
我們都知道數據一致性分為:強一致性咸灿、弱一致性构订、最終一致性。
而 MQ 為了性能考慮使用的是最終一致性避矢,那么必定會出現數據不一致的問題悼瘾。這類問題大概率是因為消費者讀取消息后,業(yè)務邏輯處理失敗導致的审胸。這時候可以增加重試機制亥宿。重試分為同步重試和異步重試。
有些消息量比較小的業(yè)務場景砂沛,可以采用同步重試烫扼。在消費消息時如果處理失敗,立刻重試 3-5 次碍庵,如果還是失敗則寫入到記錄表中映企。但如果消息量比較大,則不建議使用這種方式怎抛。因為如果出現網絡異常卑吭,可能會導致大量的消息不斷重試,影響消息讀取速度造成消息堆積马绝。
消息量比較大的業(yè)務場景豆赏,建議采用異步重試。在消費者處理失敗之后富稻,立刻寫入重試表掷邦,有個 job(如采用xxljob) 專門定時重試。
還有一種做法:如果消費失敗椭赋,自己給同一個 topic 發(fā)一條消息抚岗。在后面的某個時間點,自己又會消費到那條消息哪怔,起到了重試的效果宣蔚。如果對消息順序要求不高的場景,可以使用這種方式认境。
2.5 消息丟失問題
2.5.1 問題場景
同樣消息丟失問題胚委,也是 MQ 中普遍存在的問題,不管你用哪種 MQ 都無法避免叉信。有哪些場景會出現消息丟失問題呢亩冬?
- 生產者產生消息時,由于網絡原因發(fā)送到 MQ 失敗了硼身;
- MQ 服務器持久化硅急,存儲磁盤時出現異常覆享;
- Kafka和RocketMQ 的 offset 被回調時,略過了很多消息营袜;
- 消費者剛讀取消息撒顿,已經 ACK 確認,但業(yè)務還沒處理完连茧,服務就被重啟了核蘸。
導致消息丟失問題的原因挺多的, 生產者啸驯、 MQ 服務器客扎、 消費者都有可能產生問題。我在這里就不一一列舉了罚斗。最終的結果會導致消費者無法正確的處理消息徙鱼,而導致數據不一致的情況。
2.5.2 解決方案
不管你是否承認针姿,有時候消息真的會丟袱吆。即使這種概率非常小,也會對業(yè)務有影響距淫。生產者绞绒、MQ 服務器、消費者都有可能會導致消息丟失的問題榕暇。為了解決這個問題蓬衡,我們可以增加一張消息發(fā)送表。
當生產者發(fā)完消息之后彤枢,會往該表中寫入一條數據狰晚,狀態(tài) status 標記為待確認;
如果消費者讀取消息之后缴啡,調用生產者的 API 更新該消息的status為已確認壁晒;
-
有個job(xxljob) 每隔一段時間檢查一次消息發(fā)送表,如果5分鐘(這個時間可以根據實際情況來定)后還有狀態(tài)是待確認的消息业栅,則認為該消息已經丟失了秒咐,重新發(fā)條消息。
image.png
這樣不管是由于生產者碘裕、 MQ服務器反镇、還是消費者導致的消息丟失問題,job 都會重新發(fā)消息娘汞。
2.6 消息順序問題
2.6.1 問題場景
有些業(yè)務數據是有狀態(tài)的,比如訂單有下單夕玩、支付你弦、完成惊豺、退貨等狀態(tài)。 如果訂單數據作為消息體禽作,就會涉及順序問題了尸昧。
例如消費者收到同一個訂單的兩條消息。第一條消息的狀態(tài)是下單旷偿,第二條消息的狀態(tài)是支付烹俗,這是沒問題的。但如果第一條消息的狀態(tài)是支付萍程,第二條消息的狀態(tài)是下單就會有問題了幢妄。沒有下單就先支付了?
消息順序問題是一個非常棘手的問題茫负,比如:
Kafka 同一個 partition 中能保證順序蕉鸳,但是不同的 partition 無法保證順序;
RabbitMQ的同一個queue能夠保證順序忍法,但是如果多個消費者同一個queue 也會有順序問題潮尝。
如果消費者使用多線程消費消息,也無法保證順序饿序。
如果消費消息時同一個訂單的多條消息中勉失,中間的一條消息出現異常情況,順序將會被打亂原探。
還有如果生產者發(fā)送到 MQ中的路由規(guī)則乱凿,跟消費者不一樣,也無法保證順序踢匣。
2.6.2 解決方案
消息順序問題是一種常見問題告匠。我們以 Kafka 消費訂單消息為例,訂單有下單离唬、 支付后专、 完成、 退貨等狀態(tài)输莺。這些狀態(tài)是有先后順序的戚哎,如果順序錯了會導致業(yè)務異常。
解決這類問題之前嫂用,我們需要先確認:消費者是否真的需要知道中間狀態(tài)型凳,只知道最終狀態(tài)行不行?
其實很多時候嘱函,我真的需要知道的是最終狀態(tài)甘畅。這時可以把流程優(yōu)化一下:
這種方式可以解決大部分的消息順序問題。
但如果真的有需要保證消息順序的需求,那么可以將訂單號路由到不同的 partition疏唾。同一個訂單號的消息蓄氧,每次到發(fā)到同一個partition。
2.7 消息堆積
2.7.1 問題場景
如果消息消費者讀取消息的速度槐脏,能夠跟上消息生產者的節(jié)奏喉童,那么整套 MQ 機制就能發(fā)揮最大作用。
但是很多時候顿天,由于某些批處理或者其他原因堂氯,導致消費速度小于生產速度。這樣會直接導致消息堆積問題牌废,從而影響業(yè)務功能咽白。
這里以下單 開通會員為例,如果消息出現堆積會導致用戶下單之后畔规,很久之后才能變成會員局扶。這種情況肯定會引起大量用戶投訴。
2.7.2 解決方案
那么消息堆積問題該如何解決呢叁扫?這個要看消息是否需要保證順序三妈。如果不需要保證順序,可以讀取消息之后用多線程處理業(yè)務邏輯莫绣。
這樣就能增加業(yè)務邏輯處理速度畴蒲,解決消息堆積問題。但是線程池的核心線程數和最大線程數需要合理配置对室,不然可能會浪費系統(tǒng)資源模燥。
如果需要保證順序,可以讀取消息之后將消息按照一定的規(guī)則分發(fā)到多個隊列中掩宜,然后在隊列中用單線程處理蔫骂。
資料來源:
面試官竟然問我為啥要用MQ,幸虧我看了參考答案
作者:陳琰AC
鏈接:http://www.reibang.com/p/439f74dd62a9