分布式事務——消息最終一致性方案

前言

隨著分布式服務架構的流行與普及,原來在單體應用中執(zhí)行的多個邏輯操作,現(xiàn)在被拆分成了多個服務之間的遠程調(diào)用。雖然服務化為我們的系統(tǒng)帶來了水平伸縮的能力,然而隨之而來挑戰(zhàn)就是分布式事務問題,多個服務之間使用自己單獨維護的數(shù)據(jù)庫,它們彼此之間不在同一個事務中,假如A執(zhí)行成功了,B執(zhí)行卻失敗了,而A的事務此時已經(jīng)提交,無法回滾,那么最終就會導致兩邊數(shù)據(jù)不一致性的問題;盡管很早之前就有基于兩階段提交的XA分布式事務,但是這類方案因為需要資源的全局鎖定,導致性能極差;因此后面就逐漸衍生出了消息最終一致性、TCC等柔性事務的分布式事務方案,本文主要分析的是基于消息的最終一致性方案

普通消息的處理流程

普通消息處理流程.png
  1. 消息生成者發(fā)送消息
  2. MQ收到消息,將消息進行持久化,在存儲中新增一條記錄
  3. 返回ACK給消費者
  4. MQ push 消息給對應的消費者,然后等待消費者返回ACK
  5. 如果消息消費者在指定時間內(nèi)成功返回ack,那么MQ認為消息消費成功,在存儲中刪除消息,即執(zhí)行第6步;如果MQ在指定時間內(nèi)沒有收到ACK,則認為消息消費失敗,會嘗試重新push消息,重復執(zhí)行4、5、6步驟
  6. MQ刪除消息

普通消息處理存在的一致性問題

我們以訂單創(chuàng)建為例,訂單系統(tǒng)先創(chuàng)建訂單(本地事務),再發(fā)送消息給下游處理;如果訂單創(chuàng)建成功,然而消息沒有發(fā)送出去,那么下游所有系統(tǒng)都無法感知到這個事件,會出現(xiàn)臟數(shù)據(jù);

public void processOrder() {
    // 訂單處理(業(yè)務操作) 
    orderService.process();
    // 發(fā)送訂單處理成功消息(發(fā)送消息) 
    sendBizMsg ();
}

如果先發(fā)送訂單消息,再創(chuàng)建訂單;那么就有可能消息發(fā)送成功,但是在訂單創(chuàng)建的時候卻失敗了,此時下游系統(tǒng)卻認為這個訂單已經(jīng)創(chuàng)建,也會出現(xiàn)臟數(shù)據(jù)。

public void processOrder() {
   // 發(fā)送訂單處理成功消息(發(fā)送消息) 
    sendBizMsg ();
    // 訂單處理(業(yè)務操作) 
    orderService.process();
}

一個錯誤的想法

此時可能有同學會想,我們可否將消息發(fā)送和業(yè)務處理放在同一個本地事務中來進行處理,如果業(yè)務消息發(fā)送失敗,那么本地事務就回滾,這樣是不是就能解決消息發(fā)送的一致性問題呢?

@Transactionnal
public void processOrder() {
    try{
        // 訂單處理(業(yè)務操作) 
        orderService.process(); 
        // 發(fā)送訂單處理成功消息(發(fā)送消息) 
        sendBizMsg ();
    }catch(Exception e){
         事務回滾;   
    }
}

消息發(fā)送的異常情況分析

可能的情況 一致性
訂單處理成功,然后突然宕機,事務未提交,消息沒有發(fā)送出去 一致
訂單處理成功,由于網(wǎng)絡原因或者MQ宕機,消息沒有發(fā)送出去,事務回滾 一致
訂單處理成功,消息發(fā)送成功,但是MQ由于其他原因,導致消息存儲失敗,事務回滾 一致
訂單處理成功,消息存儲成功,但是MQ處理超時,從而ACK確認失敗,導致發(fā)送方本地事務回滾 不一致

從上面的情況分析,我們可以看到,使用普通的處理方式,無論如何,都無法保證業(yè)務處理與消息發(fā)送兩邊的一致性,其根本的原因就在于:遠程調(diào)用,結果最終可能為成功、失敗、超時;而對于超時的情況,處理方最終的結果可能是成功,也可能是失敗,調(diào)用方是無法知曉的。 筆者就曾經(jīng)在項目中出現(xiàn)類似的情況,調(diào)用方先在本地寫數(shù)據(jù),然后發(fā)起RPC服務調(diào)用,但是處理方由于DB數(shù)據(jù)量比較大,導致處理超時,調(diào)用方在出現(xiàn)超時異常后,直接回滾本地事務,從而導致調(diào)用方這邊沒數(shù)據(jù),而處理方那邊數(shù)據(jù)卻已經(jīng)寫入了,最終導致兩邊業(yè)務數(shù)據(jù)的不一致。為了保證兩邊數(shù)據(jù)的一致性,我們只能從其他地方尋找新的突破口。

事務消息

由于傳統(tǒng)的處理方式無法解決消息生成者本地事務處理成功消息發(fā)送成功兩者的一致性問題,因此事務消息就誕生了,它實現(xiàn)了消息生成者本地事務與消息發(fā)送的原子性,保證了消息生成者本地事務處理成功與消息發(fā)送成功的最終一致性問題。

事務消息處理的流程

事務消息處理流程.png
  1. 事務消息與普通消息的區(qū)別就在于消息生產(chǎn)環(huán)節(jié),生產(chǎn)者首先預發(fā)送一條消息到MQ(這也被稱為發(fā)送half消息)

  2. MQ接受到消息后,先進行持久化,則存儲中會新增一條狀態(tài)為待發(fā)送的消息

  3. 然后返回ACK給消息生產(chǎn)者,此時MQ不會觸發(fā)消息推送事件

  4. 生產(chǎn)者預發(fā)送消息成功后,執(zhí)行本地事務

  5. 執(zhí)行本地事務,執(zhí)行完成后,發(fā)送執(zhí)行結果給MQ

  6. MQ會根據(jù)結果刪除或者更新消息狀態(tài)為可發(fā)送

  7. 如果消息狀態(tài)更新為可發(fā)送,則MQ會push消息給消費者,后面消息的消費和普通消息是一樣的

注意點:由于MQ通常都會保證消息能夠投遞成功,因此,如果業(yè)務沒有及時返回ACK結果,那么就有可能造成MQ的重復消息投遞問題。因此,對于消息最終一致性的方案,消息的消費者必須要對消息的消費支持冪等,不能造成同一條消息的重復消費的情況。

事務消息異常情況分析

異常情況 一致性 處理異常方法
消息未存儲,業(yè)務操作未執(zhí)行 一致
存儲待發(fā)送消息成功,但是ACK失敗,導致業(yè)務未執(zhí)行(可能是MQ處理超時、網(wǎng)絡抖動等原因) 不一致 MQ確認業(yè)務操作結果,處理消息(刪除消息)
存儲待發(fā)送消息成功,ACK成功,業(yè)務執(zhí)行(可能成功也可能失敗),但是MQ沒有收到生產(chǎn)者業(yè)務處理的最終結果 不一致 MQ確認業(yè)務操作結果,處理消息(根據(jù)就業(yè)務處理結果,更新消息狀態(tài),如果業(yè)務執(zhí)行成功,則投遞消息,失敗則刪除消息)
業(yè)務處理成功,并且發(fā)送結果給MQ,但是MQ更新消息失敗,導致消息狀態(tài)依舊為待發(fā)送 不一致 同上

支持事務消息的MQ

現(xiàn)在目前較為主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事務消息。據(jù)筆者了解,早年阿里對MQ增加事務消息也是因為支付寶那邊因為業(yè)務上的需求而產(chǎn)生的。因此,如果我們希望強依賴一個MQ的事務消息來做到消息最終一致性的話,在目前的情況下,技術選型上只能去選擇RocketMQ來解決。上面我們也分析了事務消息所存在的異常情況,即MQ存儲了待發(fā)送的消息,但是MQ無法感知到上游處理的最終結果。對于RocketMQ而言,它的解決方案非常的簡單,就是其內(nèi)部實現(xiàn)會有一個定時任務,去輪訓狀態(tài)為待發(fā)送的消息,然后給producer發(fā)送check請求,而producer必須實現(xiàn)一個check監(jiān)聽器,監(jiān)聽器的內(nèi)容通常就是去檢查與之對應的本地事務是否成功(一般就是查詢DB),如果成功了,則MQ會將消息設置為可發(fā)送,否則就刪除消息。

常見的問題

  1. 問:如果預發(fā)送消息失敗,是不是業(yè)務就不執(zhí)行了?

    答:是的,對于基于消息最終一致性的方案,一般都會強依賴這步,如果這個步驟無法得到保證,那么最終也 就不可能做到最終一致性了。

  2. 問:為什么要增加一個消息預發(fā)送機制,增加兩次發(fā)布出去消息的重試機制,為什么不在業(yè)務成功之后,發(fā)送失敗的話使用一次重試機制?

    答:如果業(yè)務執(zhí)行成功,再去發(fā)消息,此時如果還沒來得及發(fā)消息,業(yè)務系統(tǒng)就已經(jīng)宕機了,系統(tǒng)重啟后,根本沒有記錄之前是否發(fā)送過消息,這樣就會導致業(yè)務執(zhí)行成功,消息最終沒發(fā)出去的情況。

  3. 如果consumer消費失敗,是否需要producer做回滾呢?

    答:這里的事務消息,producer不會因為consumer消費失敗而做回滾,采用事務消息的應用,其所追求的是高可用最終一致性,消息消費失敗的話,MQ自己會負責重推消息,直到消費成功。因此,事務消息是針對生產(chǎn)端而言的,而消費端,消費端的一致性是通過MQ的重試機制來完成的。

  4. 如果consumer端因為業(yè)務異常而導致回滾,那么豈不是兩邊最終無法保證一致性?

    答:基于消息的最終一致性方案必須保證消費端在業(yè)務上的操作沒障礙,它只允許系統(tǒng)異常的失敗,不允許業(yè)務上的失敗,比如在你業(yè)務上拋出個NPE之類的問題,導致你消費端執(zhí)行事務失敗,那就很難做到一致了。

由于并非所有的MQ都支持事務消息,假如我們不選擇RocketMQ來作為系統(tǒng)的MQ,是否能夠做到消息的最終一致性呢?答案是可以的。

基于本地消息的最終一致性

基于本地消息最終一致性.png

基于本地消息的最終一致性方案的最核心做法就是在執(zhí)行業(yè)務操作的時候,記錄一條消息數(shù)據(jù)到DB,并且消息數(shù)據(jù)的記錄與業(yè)務數(shù)據(jù)的記錄必須在同一個事務內(nèi)完成,這是該方案的前提核心保障。在記錄完成后消息數(shù)據(jù)后,后面我們就可以通過一個定時任務到DB中去輪訓狀態(tài)為待發(fā)送的消息,然后將消息投遞給MQ。這個過程中可能存在消息投遞失敗的可能,此時就依靠重試機制來保證,直到成功收到MQ的ACK確認之后,再將消息狀態(tài)更新或者消息清除;而后面消息的消費失敗的話,則依賴MQ本身的重試來完成,其最后做到兩邊系統(tǒng)數(shù)據(jù)的最終一致性。基于本地消息服務的方案雖然可以做到消息的最終一致性,但是它有一個比較嚴重的弊端,每個業(yè)務系統(tǒng)在使用該方案時,都需要在對應的業(yè)務庫創(chuàng)建一張消息表來存儲消息。針對這個問題,我們可以將該功能單獨提取出來,做成一個消息服務來統(tǒng)一處理,因而就衍生出了我們下面將要討論的方案。

獨立消息服務的最終一致性

獨立消息服務最終一致性.png

獨立消息服務最終一致性本地消息服務最終一致性最大的差異就在于將消息的存儲單獨地做成了一個RPC的服務,這個過程其實就是模擬了事務消息的消息預發(fā)送過程,如果預發(fā)送消息失敗,那么生產(chǎn)者業(yè)務就不會去執(zhí)行,因此對于生產(chǎn)者的業(yè)務而言,它是強依賴于該消息服務的。不過好在獨立消息服務支持水平擴容,因此只要部署多臺,做成HA的集群模式,就能夠保證其可靠性。在消息服務中,還有一個單獨地定時任務,它會定期輪訓長時間處于待發(fā)送狀態(tài)的消息,通過一個check補償機制來確認該消息對應的業(yè)務是否成功,如果對應的業(yè)務處理成功,則將消息修改為可發(fā)送,然后將其投遞給MQ;如果業(yè)務處理失敗,則將對應的消息更新或者刪除即可。因此在使用該方案時,消息生產(chǎn)者必須同時實現(xiàn)一個check服務,來供消息服務做消息的確認。對于消息的消費,該方案與上面的處理是一樣,都是通過MQ自身的重發(fā)機制來保證消息被消費。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內(nèi)容