淺談,分佈式事務與解決方案

原文鏈接:https://www.javazhiyin.com/573.html

分佈式事務與主流解決方案之理論篇

 

前言

  分佈式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分佈式系統的不同節點之上。簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分佈在不同的服務器上,且屬於不同的應用,分佈式事務需要保證這些小操作要麼全部成功,要麼全部失敗。本質上來說,分佈式事務就是爲了保證不同數據庫的數據一致性。

 

產生原因

數據庫分庫分表:

​ 當數據庫單表一年產生的數據超過1000W,那麼就要考慮分庫分表(具體分庫分表的原理在此不做解釋),簡單的說就是原來的一個數據庫變成了多個數據庫。這時候,如果一個操作既訪問01庫,又訪問02庫,而且要保證數據的一致性,那麼就要用到分佈式事務。

應用SOA化:

​ 所謂的SOA化,就是業務的服務化。比如原來單機支撐了整個電商網站,現在對整個網站進行拆解,分離出了訂單中心、用戶中心、庫存中心。對於訂單中心,有專門的數據庫存儲訂單信息,用戶中心也有專門的數據庫存儲用戶信息,庫存中心也會有專門的數據庫存儲庫存信息。這時候如果要同時對訂單和庫存進行操作,那麼就會涉及到訂單數據庫和庫存數據庫,爲了保證數據一致性,就需要用到分佈式事務。

​ 分佈式事務是企業集成中的一個技術難點,也是每一個分佈式系統架構中都會涉及到的一個東西,特別是在微服務架構中,幾乎可以說是無法避免。

 

應用場景

支付、轉賬:

​ 最經典的場景就是支付了,一筆支付,是對買家賬戶進行扣款,同時對賣家賬戶進行加錢,這些操作必須在一個事務裏執行,要麼全部成功,要麼全部失敗。而對於買家賬戶屬於買家中心,對應的是買家數據庫,而賣家賬戶屬於賣家中心,對應的是賣家數據庫,對不同數據庫的操作必然需要引入分佈式事務。

在線下單:

​ 買家在電商平臺下單,往往會涉及到兩個動作,一個是扣庫存,第二個是更新訂單狀態,庫存和訂單一般屬於不同的數據庫,需要使用分佈式事務保證數據一致性。

電商場景:流量充值業務

​ 中國移動-流量充值能力中心,核心業務流程爲:

​ 1、用戶進入流量充值商品購買頁面,選擇流量商品;

​ 2、購買流量充值商品,有庫存限制則判斷庫存,生成流量購買訂單;

​ 3、選擇對應的支付方式(和包、銀聯、支付寶、微信)進行支付操作;

​ 4、支付成功後,近實時流量到賬即可使用流量商品;

​ 此業務流程看似不是很複雜對吧,不涉及到類似電商業務的實物購買,但是我認爲其中的區別並不是很大,只是缺少電商中的物流發貨流程,其他流程幾乎是一樣的,也有庫存以及優惠折扣等業務存在。

 

數據庫事務

​ 事務是由一組SQL語句組成的邏輯處理單元,事務具有以下4個屬性,通常簡稱爲事務的ACID屬性:

​ 原子性(Atomicity):事務是一個原子操作單元,其對數據的修改,要麼全都執行,要麼全都不執行。

​ 一致性(Consistent):在事務開始和完成時,數據都必須保持一致狀態。這意味着所有相關的數據規則都必須應用於事務的修改,以保持數據的完整性;事務結束時,所有的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的。

​ 隔離性(Isoation):數據庫系統提供一定的隔離機制,保證事務在不受外部併發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然。

​ 持久性(Durabe):事務完成之後,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。

事務類型:

​ JDBC事務:即爲上面說的數據庫事務中的本地事務,通過connection對象控制管理。

​ JTA事務:JTA指Java事務API(JavaTransaction API),是Java EE數據庫事務規範, JTA只提供了事務管理接口,由應用程序服務器廠商(如WebSphere Application Server)提供實現,JTA事務比JDBC更強大,支持分佈式事務。

隔離級別及引發現象:(略談)

Spring事務傳播行爲:(略談)

PROPAGATION_REQUIRED:支持當前事務,如當前沒有事務,則新建一個。

PROPAGATION_SUPPORTS:支持當前事務,如當前沒有事務,則已非事務性執行(源碼中提示有個注意點,看不太明白,留待後面考究)。

PROPAGATION_MANDATORY:支持當前事務,如當前沒有事務,則拋出異常(強制一定要在一個已經存在的事務中執行,業務方法不可獨自發起自己的事務)。

PROPAGATION_REQUIRES_NEW:始終新建一個事務,如當前原來有事務,則把原事務掛起。

PROPAGATION_NOT_SUPPORTED:不支持當前事務,始終已非事務性方式執行,如當前事務存在,掛起該事務。

PROPAGATION_NEVER:不支持當前事務;如果當前事務存在,則引發異常。

PROPAGATION_NESTED:如果當前事務存在,則在嵌套事務中執行,如果當前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作(注意:當應用到JDBC時,只適用JDBC 3.0以上驅動)。

事務種類:

​ 本地事務:普通事務,獨立一個數據庫,能保證在該數據庫上操作的ACID。

​ 分佈式事務:涉及兩個或多個數據庫源的事務,即跨越多臺同類或異類數據庫的事務(由每臺數據庫的本地事務組成的),分佈式事務旨在保證這些本地事務的所有操作的ACID,使事務可以跨越多臺數據庫。

 

如何保證強一致性

本地事務(mysql 之 InnoDB):

​ InnoDB支持事務,同Oracle類似,事務提交需要寫redo、undo。採用日誌先行的策略,將數據的變更在內存中完成,並且將事務記錄成redo,順序的寫入redo日誌中,即表示該事務已經完成,就可以返回給客戶已提交的信息。但是實際上被更改的數據還在內存中,並沒有刷新到磁盤,即還沒有落地,當達到一定的條件,會觸發checkpoint,將內存中的數據(page)合併寫入到磁盤,這樣就減少了離散寫、IOPS,提高性能。

​ 在這個過程中,如果服務器宕機了,內存中的數據丟失,當重啓後,會通過redo日誌進行recovery重做。確保不會丟失數據。因此只要redo能夠實時的寫入到磁盤,InnoDB就不會丟數據。

分佈式事務:

​ 多個數據庫中的某個數據庫在提交事務的時候突然斷電,那麼它是怎麼樣恢復的呢? 這也是分佈式系統複雜的地方,因爲分佈式的網絡環境很複雜,這種“斷電”故障要比單機多很多,所以我們在做分佈式系統的時候,最先考慮的就是這種情況。這些異常可能有 機器宕機、網絡異常、消息丟失、消息亂序、數據錯誤、不可靠的TCP、存儲數據丟失、其他異常等等...

​ 對分佈式系統有過研究的讀者, 聽說過 "CAP定律"、"Base理論" 等,這裏不對這些概念做過多的解釋,有興趣的讀者可以查看相關參考資料 。

​ 在分佈式系統中,同時滿足 "CAP定律" 中的 "一致性"、"可用性" 和 "分區容錯性" 三者是不可能的, 根據不同的業務場景使用不同的方法實現最終一致性,可以根據業務的特性做部分取捨,在業務過程中可以容忍一定時間內的數據不一致。

 

實現分佈式事務解決方案

基於XA協議的兩階段提交(2PC)

​ XA 是由 X/Open 組織提出的分佈式事務的規範

​ XA 規範主要 定義了 ( 全局 ) 事務管理器 ( Transaction Manager ) 和 ( 局部 ) 資源管理器 ( Resource Manager ) 之間的接口。 XA 接口是雙向的系統接口,在事務管理器(Transaction Manager)以及一個或多個資源管理器(Resource Manager)之間形成通信橋樑。 XA 之所以需要引入事務管理器是因爲,在分佈式系統中,從理論上講(參考Fischer等的論文),兩臺機器理論上無 法達到一致的狀態,需要引入一個單點進行協調。 事務管理器控制着全局事務,管理事務生命週期,並協調資源。資源管理器負責控制和管理實際資源(如數據庫或 JMS隊列)。下圖說明了事務管理器、資源管理器,與應用程序之間的關係:

在 JavaEE 平臺下,WebLogic、Webshare 等主流商用的應用服務器提供了 JTA 的實現和支持。而在 Tomcat 下是沒有實現的(Tomcat 不能算是 JavaEE 應用服務器,比較輕量),這就需要藉助第三方的框架 Jotm、Automikos 等來實現,兩者均支持 Spring 事務整合。

​ 在分佈式事務的控制中採用了兩階段提交協議(Two- Phase Commit Protocol)。即事務的提交分爲兩個階段:

  預提交階段(Pre-Commit Phase)  決策後階段(Post-Decision Phase)

​ 爲了支持兩階段提交,一個分佈式更新事務中涉及到的服務器必須能夠相互通信。一般來說一個服務器會被指定爲"控制"或"提交"服務器並監控來自其它服務器的信息。

​ 在一個分佈式事務中,必須有一個場地的Server作爲協調者(coordinator),它能向 其它場地的Server發出請求,並對它們的回答作出響應,由它來控制一個分佈式事務的提交或撤消。該分佈式事務中涉及到的其它場地的Server稱爲參 與者(Participant)。

事務兩階段提交的過程如下:  

● 兩階段提交在應用程序向協調者發出一個提交命令時被啓動。這時提交進入第一階段,即預提交階段。在這一階段中:

(1) 協調者準備局部(即在本地)提交併在日誌中寫入"預提交"日誌項,幷包含有該事務的所有參與者的名字。

(2) 協調者詢問參與者能否提交該事務。一個參與者可能由於多種原因不能提交。例如,該Server提供的約束條件(Constraints)的延遲檢查不符合 限制條件時,不能提交;參與者本身的Server進程或硬件發生故障,不能提交;或者協調者訪問不到某參與者(網絡故障),這時協調者都認爲是收到了一個 否定的回答。

(3) 如果參與者能夠提交,則在其本身的日誌中寫入"準備提交"日誌項,該日誌項立即寫入硬盤,然後給協調者發回,已準備好提交"的回答。

(4) 協調者等待所有參與者的回答,如果有參與者發回否定的回答,則協調者撤消該事務並給所有參與者發出一個"撤消該事務"的消息,結束該分佈式事務,撤消該事務的所有影響。

● 如果所有的參與者都送回"已準備好提交"的消息,則該事務的提交進入第二階段,即決策後提交階段。在這一階段中:  

(1) 協調者在日誌中寫入"提交"日誌項,並立即寫入硬盤。  

(2) 協調者向參與者發出"提交該事務"的命令。各參與者接到該命令後,在各自的日誌中寫入"提交"日誌項,並立即寫入硬盤。然後送回"已提交"的消息,釋放該事務佔用的資源。   

(3) 當所有的參與者都送回"已提交"的消息後,協調者在日誌中寫入"事務提交完成"日誌項,釋放協調者佔用的資源 。這樣,完成了該分佈式事務的提交。

優點: 儘量保證了數據的強一致,適合對數據強一致要求很高的關鍵領域。

缺點: 實現複雜,犧牲了可用性,對性能影響較大,涉及多次節點間的網絡通信,通信時間太長,不適合高併發高性能場景。

補償事務(TCC)

​ TCC 其實就是採用的補償機制,其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。它分爲三個階段:

  • Try 階段主要是對業務系統做檢測及資源預留
  • Confirm 階段主要是對業務系統做確認提交,Try 階段執行成功並開始執行 Confirm 階段時,默認Confirm 階段是不會出錯的。即:只要 Try 成功,Confirm 一定成功。
  • Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放。

舉個例子,假入 Bob 要向 Smith 轉賬,思路大概是:我們有一個本地方法,裏面依次調用

1、首先在 Try 階段,要先調用遠程接口把 Smith 和 Bob 的錢給凍結起來。

2、在 Confirm 階段,執行遠程調用的轉賬的操作,轉賬成功進行解凍。

3、如果第2步執行成功,那麼轉賬成功,如果第二步執行失敗,則調用遠程凍結接口對應的解凍方法 (Cancel)。

優點: 跟 2PC 比起來,實現以及流程相對簡單了一些,但數據的一致性比 2PC 也要差一些。

缺點: 缺點還是比較明顯的,在2,3步中都有可能失敗。TCC 屬於應用層的一種補償方式,所以需要程序員在實現的時候多寫很多補償的代碼,在一些場景中,一些業務流程可能用 TCC 不太好定義及處理。

本地消息表(MQ 異步確保)

​ 這種實現方式的思路,其實是源於ebay,後來通過支付寶等公司的佈道,在業內廣泛使用。其基本的設計思想是將遠程分佈式事務拆分成一系列的本地事務。如果不考慮性能及設計優雅,藉助關係型數據庫中的表即可實現。

基本思路就是:

​ 消息生產方,需要額外建一個消息表,並記錄消息發送狀態。消息表和業務數據要在一個事務裏提交,也就是說他們要在一個數據庫裏面。然後消息會經過 MQ 發送到消息的消費方。如果消息發送失敗,會進行重試發送。

​ 消息消費方,需要處理這個消息,並完成自己的業務邏輯。此時如果本地事務處理成功,表明已經處理成功了,如果處理失敗,那麼就會重試執行。如果是業務上面的失敗,可以給生產方發送一個業務補償消息,通知生產方進行回滾等操作。

​ 生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。如果有靠譜的自動對賬補賬邏輯(防止消息會被重複投遞,增加消息應用狀態表(message_apply),通俗來說就是個賬本,用於記錄消息的消費情況,每次來一個消息, 在真正執行之前,先去消息應用狀態表中查詢一遍,如果找到說明是重複消息,丟棄即可,如果沒找到才執行,同時插入到消息應用狀態表(同一事務)),這種方案還是非常實用的。

​ 這種方案遵循 BASE 理論,採用的是最終一致性,比較適合實際業務場景的,即不會出現像 2PC 那樣複雜的實現(當調用鏈很長的時候,2PC 的可用性是非常低的),也不會像 TCC 那樣可能出現確認或者回滾不了的情況。

優點: 一種非常經典的實現,避免了分佈式事務,實現了最終一致性。

缺點: 消息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理,而且,關係型數據庫的吞吐量和性能方面存在瓶頸,頻繁的讀寫消息會給數據庫造成壓力。

MQ 事務消息

​ 有一些第三方的 MQ 是支持事務消息的,比如 RocketMQ,他們支持事務消息的方式也是類似於採用的二階段提交,但是市面上一些主流的 MQ 都是不支持事務消息的,比如 RabbitMQ 和 Kafka 都不支持。

​ 以阿里的 RocketMQ 中間件爲例,其思路大致爲:

​ 第一階段 Prepared 消息,會拿到消息的地址。 第二階段執行本地事務,第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。

​ 也就是說在業務方法內要想消息隊列提交兩次請求,一次發送消息和一次確認消息。如果確認消息發送失敗了 RocketMQ 會定期掃描消息集羣中的事務消息,這時候發現了 Prepared 消息,它會向消息發送者確認,所以生產方需要實現一個 check 接口,RocketMQ 會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。

​ 這樣就保證了消息發送與本地事務同時成功或同時失敗,具體原理如下:

​ 1、A系統向消息中間件發送一條預備消息

2、消息中間件保存預備消息並返回成功

3、A執行本地事務

4、A發送提交消息給消息中間件

通過以上4 步完成了一個消息事務。對於以上的4個步驟,每個步驟都可能產生錯誤,下面一一分析:

​ 步驟一出錯,則整個事務失敗,不會執行A的本地操作

​ 步驟二出錯,則整個事務失敗,不會執行A的本地操作

​ 步驟三出錯,這時候需要回滾預備消息,怎麼回滾?答案是A系統實現一個消息中間件的回調接口,消息中間件會去不斷執行回調接口,檢查A事務執行是否執行成功,如果失敗則回滾預備消息

​ 步驟四出錯,這時候A的本地事務是成功的,那麼消息中間件要回滾A嗎?答案是不需要,其實通過回調接口,消息中間件能夠檢查到A執行成功了,這時候其實不需要A發提交消息了,消息中間件可以自己對消息進行提交,從而完成整個消息事務

​ 基於消息中間件的兩階段提交往往用在高併發場景下,將一個分佈式事務拆成一個消息事務(A系統的本地操作+發消息)+B系統的本地操作,其中B系統的操作由消息驅動,只要消息事務成功,那麼A操作一定成功,消息也一定發出來了,這時候B會收到消息去執行本地操作,如果本地操作失敗,消息會重投,直到B操作成功,這樣就變相地實現了A與B的分佈式事務。

優點: 實現了最終一致性,不需要依賴本地數據庫事務。

缺點: 實現難度大,主流MQ不支持,沒有.NET客戶端,RocketMQ事務消息部分代碼也未開源。

Sagas 事務模型

​ Saga事務模型又叫做長時間運行的事務(Long-running-transaction), 它是由普林斯頓大學的H.Garcia-Molina等人提出,它描述的是另外一種在沒有兩階段提交的的情況下解決分佈式系統中複雜的業務事務問題。

​ 我們這裏說的是一種基於 Sagas 機制的工作流事務模型,這個模型的相關理論目前來說還是比較新的,以至於百度上幾乎沒有什麼相關資料。

​ 該模型其核心思想就是拆分分佈式系統中的長事務爲多個短事務,或者叫多個本地事務,然後由 Sagas 工作流引擎負責協調,如果整個流程正常結束,那麼就算是業務成功完成,如果在這過程中實現失敗,那麼Sagas工作流引擎就會以相反的順序調用補償操作,重新進行業務回滾。

​ 比如我們一次關於購買旅遊套餐業務操作涉及到三個操作,他們分別是預定車輛,預定賓館,預定機票,他們分別屬於三個不同的遠程接口。可能從我們程序的角度來說他們不屬於一個事務,但是從業務角度來說是屬於同一個事務的。

​ 他們的執行順序如上圖所示,所以當發生失敗時,會依次進行取消的補償操作。

​ 因爲長事務被拆分了很多個業務流,所以 Sagas 事務模型最重要的一個部件就是工作流或者你也可以叫流程管理器(Process Manager),工作流引擎和Process Manager雖然不是同一個東西,但是在這裏,他們的職責是相同的。

​ 優缺點這裏我們就不說了,因爲這個理論比較新,目前市面上還沒有什麼解決方案,即使是 Java 領域,也沒有搜索的太多有用的信息。

其他補償方式

​ 做過支付寶交易接口的同學都知道,我們一般會在支付寶的回調頁面和接口裏,解密參數,然後調用系統中更新交易狀態相關的服務,將訂單更新爲付款成功。同時,只有當我們回調頁面中輸出了success字樣或者標識業務處理成功相應狀態碼時,支付寶纔會停止回調請求。否則,支付寶會每間隔一段時間後,再向客戶方發起回調請求,直到輸出成功標識爲止。

​ 其實這就是一個很典型的補償例子,跟一些 MQ 重試補償機制很類似。

​ 一般成熟的系統中,對於級別較高的服務和接口,整體的可用性通常都會很高。如果有些業務由於瞬時的網絡故障或調用超時等問題,那麼這種重試機制其實是非常有效的。

​ 當然,考慮個比較極端的場景,假如系統自身有bug或者程序邏輯有問題,那麼重試1W次那也是無濟於事的。那豈不是就發生了“明明已經付款,卻顯示未付款不發貨”類似的悲劇?

​ 其實爲了交易系統更可靠,我們一般會在類似交易這種高級別的服務代碼中,加入詳細日誌記錄的,一旦系統內部引發類似致命異常,會有郵件通知。同時,後臺會有定時任務掃描和分析此類日誌,檢查出這種特殊的情況,會嘗試通過程序來補償並郵件通知相關人員。

​ 在某些特殊的情況下,還會有 "人工補償" 的,這也是最後一道屏障。

總結

​ 分佈式事務,本質上是對多個數據庫的事務進行統一控制,按照控制力度可以分爲:不控制、部分控制和完全控制。

​ 具體用哪種方式,最終還是取決於業務場景。作爲技術人員,一定不能忘了技術是爲業務服務的,不要爲了技術而技術,針對不同業務進行技術選型也是一種很重要的能力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章