分佈式事務:基於可靠消息服務

微服務倡導將複雜的單體應用拆分爲若干個功能簡單、鬆耦合的服務,而於此同時就會引入多個服務之間的分佈式事務的問題。git

衆所周知,數據庫能實現本地事務,也就是說在同一個數據庫中,能夠保證事務的原子性,就是所有成功或者失敗,上篇文章也寫過簡單的多數據源事務的解決方案(相似2PC)github

但如今的系統每每採用微服務架構,業務系統擁有獨立的數據庫,所以就出現了跨多個數據庫的事務需求,這種事務即爲「分佈式事務」。redis

針對這樣的問題通常經常使用的方案有:數據庫

  • 2PC/3PC (兩階段提交協議/三階段提交協議)
  • TCC (補償型)
  • 基於可靠消息服務的分佈式事務(異步確保型)

總體流程

基於可靠消息服務的分佈式事務,我本身實現了一個基於rabbitmq的分佈式事務中間件,shine-mq架構

一開始原本是想用來封裝mq的操做方便使用,後續迭代增長了分佈式事務的功能。下面就來介紹下這個中間件:併發

shine-mq

  • 在服務A處理任務A前,首先向Coordinator發送一條prepare(攜帶回查id)記錄,表示要開始這個分佈式任務
  • Coordinator持久化prepare記錄後響應服務A
  • 服務A收到確認應答後,服務A處理任務A,成功後發送一條ready記錄,Coordinator將刪除以前對應的prepare記錄,並持久化ready記錄和完整的消息
  • 服務A在收到ready記錄和消息持久化的應答後,就能夠提交消息到消息中間件了,針對rabbitmq能夠設置setPublisherConfirms(true)以及實現setConfirmCallback的回調來實現消息中間件持久化應答服務A。這以後對於服務A來講就能夠刪除以前的ready記錄和去處理其餘任務了。
  • 消息中間件(rabbitmq能夠經過鏡像隊列來實現高可用)在肯定將消息落盤以後就能夠向服務B投遞消息
  • 服務B消費了該消息,併成功處理了任務B,服務B再向消息中間件返回一個確認應答,告訴消息中間件該消息已經成功消費,此時,這個分佈式事務完成。

上述是整個流程,服務A完成任務A後,到任務B執行完成之間,會存在必定的時間差。在這個時間差內,整個系統處於數據不一致的狀態,但這短暫的不一致性是能夠接受的,由於通過短暫的時間後,系統又能夠保持數據一致性,知足BASE理論。運維

BASE理論:異步

  • BA:Basic Available 基本可用
  • S:Soft State:柔性狀態 同一數據的不一樣副本的狀態,能夠不須要實時一致。
  • E:Eventual Consisstency:最終一致性 同一數據的不一樣副本的狀態,能夠不須要實時一致,但必定要保證通過必定時間後仍然是一致的。

異常狀況

上面的是一個比較理想的流程,可是真正的環境會有不少突發狀況,好比任務A處理失敗,那麼須要進入回滾流程分佈式

other1

由於任務A的異常對於服務A是能夠直接捕獲的,回滾異常後刪除prepare記錄,服務A刪除以後即可以認爲回滾已經完成,即可以去作其餘的事情。微服務

而該消息沒有投遞到消息中間件,則服務B沒有影響。此時系統又處於一致性狀態,由於任務A和任務B都沒有執行。

Coordinator提供了接口能夠本身來實現,我默認實現的方式是用redis。若要使用其餘方式能夠自行實現接口。

other2

上圖表現的是發送ready記錄的時候,失敗了。這時候對於服務A是會收到異常或者收不到應答,這時候能夠直接將以前的任務A進行回滾,任務A在回滾的時候會觸發刪除ready的操做。一樣若是異常是發生在發送prepare的狀況下,這時候服務A還沒執行任務也不會有影響。

分析完服務A,Coordinator和消息中間件之間的一些狀況後,如今分析下消息中間件和服務B之間的一些特殊狀況。

當消息成功發佈到消息中間件以後,服務A就能夠作本身的事情去了,消息中間件會保證消息能成功投遞到服務B。這個就是消息中間件在消息投遞狀況下的可靠性保證,具體流程是消息中間件向下遊系統投遞完消息後便進入阻塞等待狀態,下游系統便當即進行任務的處理,任務處理完成後便向消息中間件返回應答。消息中間件收到確認應答後便認爲該事務處理完畢!若是消息在投遞過程當中丟失,或消息的確認應答在返回途中丟失,那麼消息中間件在等待確認應答超時以後就會從新投遞,直到下游消費者返回消費成功響應爲止。

這之間能夠設置消息重試的次數和時間間隔,若是一直失敗這時候就會用到死信隊列。具體看下圖:

other3

當消息一直沒法被正常消費,超過設置的重試閾值就會投遞到死信隊列,死信隊列的exchange和routeKey默認是@DistributedTrans中設置的值。

經過消費死信隊列的消息來處理這種異常狀況(能夠設置短信或郵箱提醒,人工介入),這裏暫時不實現服務A的回滾,由於讓服務A事先提供回滾接口,這無疑增長了額外的開發成本,業務系統的複雜度也將提升。對於一個業務系統的設計目標是,在保證性能的前提下,最大限度地下降系統複雜度,從而可以下降系統的運維成本。

設計思路

最後整理一下整個中間件的設計思路

上面已經分析了一些異常狀況,對於下游服務和消息中間件的原子性,咱們能夠經過消息中間件投遞的可靠性來保證(就是ACK模式,失敗或未收到應答進行重試)。 那麼咱們要實現分佈式事務,剩下的就是要保證上游服務執行的任務和向消息中間件投遞消息這2個操做的原子性。

這時候通常就會有兩種方案,同步和異步通訊。經過以前的時序圖,很顯然上游系統和消息中間件之間採用的是異步通訊,也就是說當上遊服務提交完消息後即可以去作別的事情,接下來提交、回滾就徹底交給消息中間件來完成,而且徹底信任消息中間件,認爲它必定能正確地完成事務的提交或回滾。這主要是爲了提升系統併發度,另外業務系統直接和用戶打交道,用戶體驗尤其重要,所以這種異步通訊方式可以極大程度地下降用戶等待時間。

Rabbitmq其實有提供事務機制,使用txSelect(), txCommit()以及txRollback()來實現,經過測試抓包發現,Tx.Commit後直接發送Tx.Commit-Ok,二者之間的時間間隔會比較長,簡單的測試能到300ms,這是很耗時的。因此我沒有用這種方式,而是引入一個Coordinator(協調者)來實現。

另外還有一個比較關鍵的daemon(守護線程),是處理在Coordinator一些錯誤超時的記錄(相似Rocketmq的超時詢問機制)。因此服務A除了實現正常的業務流程外,還需提供一個事務詢問的接口,供Coordinator調用,來保障服務A在執行任務出現宕機的狀況。當有prepare超時就會觸發訪問這個回查接口,該接口會返回三種結果:

  • 提交 將該消息投遞
  • 回滾 直接將條消息丟棄
  • 處理中 繼續等待,重置時間。

而超時的ready的消息,就直接撈起發送到消息中間件,由於只要是ready消息持久化到協調者,那就說明服務A的任務已經完成。

這樣就能保證上游服務和消息中間件的原子性了(具體能夠看分佈式事務:消息可靠發送),再經過消息中間件可靠的投遞結合下游服務,就完成了分佈式事務。

若是對你有幫助,那就幫忙點個星星把 ^.^

github地址:github.com/7le/shine-m…


Github 不要吝嗇你的star ^.^ 更多精彩 戳我

相關文章
相關標籤/搜索