RocketMQ進(jìn)階必學(xué):事物消息
分布式事務(wù)在微服務(wù)中是比較常見且又比較棘手的難題,當(dāng)然,它也并不是無(wú)解的,如果熟悉分布式事務(wù)的同學(xué),應(yīng)該知道 XA、2PC/3PC、TCC 、事務(wù)消息等解決方案。
事務(wù)消息是分布式事務(wù)解決方案的一種,也是我們今天要討論的主題。
什么是事務(wù)消息?
事務(wù)消息的目標(biāo),是為了實(shí)現(xiàn)可靠性消息最終一致性。
這個(gè)又是怎么理解呢?舉個(gè)例子可能大家就比較容易理解。
還是我們比較常見的支付場(chǎng)景:用戶支付下單,修改訂單狀態(tài)為完成,然后會(huì)通過(guò) MQ 發(fā)送訂單消息。
下游服務(wù)比如積分系統(tǒng)更新用戶的積分、物流系統(tǒng)需要更新訂單的物流信息、推送系統(tǒng)需要給用戶推送消息或者廣告等。
圖片
可以說(shuō)這算是一個(gè)比較標(biāo)準(zhǔn)的微服務(wù)架構(gòu),根據(jù)業(yè)務(wù)領(lǐng)域劃分不同業(yè)務(wù)系統(tǒng),各業(yè)務(wù)系統(tǒng)通過(guò) MQ 消息來(lái)實(shí)現(xiàn)訂單業(yè)務(wù)的解耦,流程上看似沒(méi)什么問(wèn)題。
但我們仔細(xì)看一下,在訂單系統(tǒng)訂單更新和發(fā)送消息的這個(gè)步驟中,實(shí)際上會(huì)發(fā)生不一致的情況:
- 訂單更新成功,消息發(fā)送失敗。
- 訂單更新成功,消息發(fā)送超時(shí)。
- 訂單更新失敗,消息發(fā)送不管成功還是失敗,整個(gè)事務(wù)都會(huì)回滾。
- 訂單更新成功,消息發(fā)送成功,業(yè)務(wù)端消費(fèi)失敗。
我們逐一來(lái)分析這幾種情況:
- 第1種訂單更新成功,消息發(fā)送失敗。這時(shí)下游的業(yè)務(wù)系統(tǒng)和支付系統(tǒng)的訂單數(shù)據(jù)是不一致的。不過(guò)我們既然明確知道是消息發(fā)送失敗,是可以對(duì)消息進(jìn)行重試或者補(bǔ)償。
- 第2種訂單更新成功,消息發(fā)送超時(shí)。這種情況我們是無(wú)法確定消息是否發(fā)送成功,如果要進(jìn)行重試或補(bǔ)償,就需要對(duì)投遞的消息進(jìn)行反查詢,不然可能會(huì)重復(fù)發(fā)送。
- 第3種情況比較簡(jiǎn)單,訂單執(zhí)行失敗,這時(shí)是拋出異常或者直接返回,都不會(huì)走到消息發(fā)送的流程。
- 第4種情況我們可以不用考慮,因?yàn)橄l(fā)送成功了之后,訂單服務(wù)就完成了自己的任務(wù),至于下游是否消費(fèi)成功,是各自業(yè)務(wù)系統(tǒng)各自負(fù)責(zé)的。
在這里其實(shí)可能有人會(huì)有疑問(wèn),包括我自己。如果把發(fā)送消息和訂單更新邏輯放在一個(gè)事務(wù)里面,執(zhí)行事務(wù)成功就發(fā)送普通消息,失敗就回滾是不是也可以?
這里就是對(duì)應(yīng)的第2種情況,如果事務(wù)執(zhí)行失敗是因?yàn)橄l(fā)送超時(shí),但是我們無(wú)法確定消息是否就真的成功發(fā)送到消息服務(wù)中。
總結(jié)一下,消息發(fā)送失敗或者超時(shí),其實(shí)已經(jīng)造成了數(shù)據(jù)不一致的情況,也就是訂單系統(tǒng)和下游的積分系統(tǒng)、物流系統(tǒng)、推送系統(tǒng)的訂單數(shù)據(jù)不一致。
而對(duì)消息進(jìn)行重試或者補(bǔ)償,是為了保證訂單數(shù)據(jù)最終能同步到下方各業(yè)務(wù)系統(tǒng),這就是所說(shuō)的消息最終一致性。
如果我們要解決這個(gè)問(wèn)題,實(shí)現(xiàn)消息最終一致性,有什么方案嗎?
增加一個(gè)消息發(fā)送記錄表,在訂單執(zhí)行成功的同時(shí),插入一條當(dāng)前訂單的消息發(fā)送記錄,消息發(fā)送成功后,再更新數(shù)據(jù)的發(fā)送狀態(tài)。如果是消息發(fā)送失敗或者超時(shí),由程序后臺(tái)任務(wù)來(lái)補(bǔ)償重發(fā)。
圖片
這個(gè)方法比較簡(jiǎn)單好理解,就是通過(guò)記錄消息發(fā)送的狀態(tài),來(lái)確保訂單更新成功后,消息最終一定能觸達(dá)到 MQ,后臺(tái)的定時(shí)任務(wù)則是對(duì)發(fā)送超時(shí)的消息補(bǔ)償。
如果對(duì)于要求消息最終一致性的業(yè)務(wù)場(chǎng)景,是不是都要這么實(shí)現(xiàn)一套繁瑣的流程呢?沒(méi)錯(cuò),這個(gè)確實(shí)很麻煩。
不過(guò),事務(wù)消息可以幫我們解決這個(gè)問(wèn)題。
事務(wù)消息原理
RocketMQ 事務(wù)消息流程??
圖片
我們結(jié)合上面用戶支付的例子,看一下使用 RocketMQ 如何實(shí)現(xiàn)事務(wù)消息:
- 首先,在訂單執(zhí)行操作之前,先發(fā)送一條半事務(wù)消息到 RocketMQ,表示即將開始執(zhí)行本地事務(wù),RocketMQ 收到這個(gè)半事務(wù)消息之后,不會(huì)馬上投遞,而是把消息暫存在臨時(shí)隊(duì)列中。
- 半消息發(fā)送成功之后,訂單系統(tǒng)就開始執(zhí)行本地事務(wù),即更新訂單的操作。執(zhí)行完成后會(huì)發(fā)送一條確認(rèn)消息到 RocketMQ 中,告知訂單執(zhí)行的結(jié)果。如果訂單執(zhí)行成功,RocketMQ 會(huì)將之前收到的半事務(wù)消息投遞到目標(biāo)隊(duì)列中,消費(fèi)端可馬上訂閱消費(fèi);如果訂單執(zhí)行失敗,RocketMQ 則會(huì)刪除之前收到的半事務(wù)消息,保證消息不會(huì)被消費(fèi)。
當(dāng)然,我們也要考慮到極端的情況:假如訂單更新后就宕機(jī)或者重啟,那么發(fā)給 RocketMQ 確認(rèn)消息就會(huì)丟掉,訂單數(shù)據(jù)還是會(huì)發(fā)送不一致的情況。
所以,這里就需要消息回查的機(jī)制。RocketMQ 收到半事務(wù)消息后,在特定的時(shí)間內(nèi)如果沒(méi)有收到確認(rèn)消息,那么 RocketMQ 會(huì)反過(guò)來(lái)請(qǐng)求本地事務(wù)執(zhí)行的最終狀態(tài)(即訂單更新的執(zhí)行結(jié)果),根據(jù)返回的執(zhí)行結(jié)果再對(duì)半事務(wù)消息做處理。
不過(guò)換個(gè)角度來(lái)看,其實(shí) RocketMQ 的事務(wù)消息與我們上面使用消息記錄表來(lái)實(shí)現(xiàn)消息一致性的方案有異曲同工之妙?;蛘呖梢哉f(shuō)其實(shí)我們已經(jīng)逆向?qū)崿F(xiàn)了這一套事務(wù)消息。
RocketMQ 自身會(huì)存儲(chǔ)消息,相當(dāng)于幫我們實(shí)現(xiàn)了消息發(fā)送的記錄;發(fā)送半消息相當(dāng)于插入一條消息發(fā)送記錄;發(fā)送確認(rèn)消息相當(dāng)于更新消息發(fā)送記錄;消息回查相當(dāng)于后臺(tái)任務(wù)的補(bǔ)償和重試。
再?gòu)牧硗庖粋€(gè)角度來(lái)看,無(wú)論是我們自己實(shí)現(xiàn)的事務(wù)消息還是 RocketMQ 的事務(wù)消息,其實(shí)都是二階段提交(2PC)的實(shí)現(xiàn)。第一階段的提交與第二階段的確認(rèn),目的都是保證本地事務(wù)與消息的一致性。
適用場(chǎng)景
事務(wù)消息是保證消息的最終一致性,在中間階段可能會(huì)出現(xiàn)數(shù)據(jù)不一致的情況。
所以適用于一些需要異步更新數(shù)據(jù),并且對(duì)數(shù)據(jù)實(shí)時(shí)性要求不太高的場(chǎng)景。
比如在用戶支付訂單后,可能會(huì)出現(xiàn)在短暫的幾秒里,用戶積分沒(méi)有變動(dòng)、購(gòu)物車的商品沒(méi)有被處理等,這些情況是可以接受的,只要保證最終用戶積分、購(gòu)物車的數(shù)據(jù)和訂單數(shù)據(jù)保持一致就可以。