分散式事務-解決方案
◆ 兩階段提交/XA
XA是由X/Open組織提出的分散式事務的規範,XA規範主要定義了(全域性)事務管理器(TM)和(區域性)資源管理器(RM)之間的介面。本地的資料庫如mysql在XA中扮演的是RM角色
XA一共分為兩階段:
第一階段(prepare):即所有的參與者RM準備執行事務並鎖住需要的資源。參與者ready時,向TM報告已準備就緒。
第二階段 (commit/rollback):當事務管理者(TM)確認所有參與者(RM)都ready後,向所有參與者傳送commit命令。
目前主流的資料庫基本都支援XA事務,包括mysql、oracle、sqlserver、postgre
XA 事務由一個或多個資源管理器(RM)、一個事務管理器(TM)和一個應用程式(ApplicationProgram)組成。
把上面的轉賬作為例子,一個成功完成的XA事務時序圖如下:
如果有任何一個參與者prepare失敗,那麼TM會通知所有完成prepare的參與者進行回滾。
XA事務的特點是:
-
簡單易理解,開發較容易
-
對資源進行了長時間的鎖定,併發度低
如果讀者想要進一步研究XA,go語言可參考DTM,java語言可參考seata
◆ SAGA
Saga是這一篇資料庫論文saga提到的一個方案。其核心思想是將長事務拆分為多個本地短事務,由Saga事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次呼叫補償操作。
把上面的轉賬作為例子,一個成功完成的SAGA事務時序圖如下:
SAGA事務的特點:
-
併發度高,不用像XA事務那樣長期鎖定資源
-
需要定義正常操作以及補償操作,開發量比XA大
-
一致性較弱,對於轉賬,可能發生A使用者已扣款,最後轉賬又失敗的情況
論文裡面的SAGA內容較多,包括兩種恢復策略,包括分支事務併發執行,我們這裡的討論,僅包括最簡單的SAGA
SAGA適用的場景較多,長事務適用,對中間結果不敏感的業務場景適用
如果讀者想要進一步研究SAGA,go語言可參考DTM,java語言可參考seata
◆ TCC
關於 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 於 2007 年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。
TCC分為3個階段
-
Try 階段:嘗試執行,完成所有業務檢查(一致性), 預留必須業務資源(準隔離性)
-
Confirm 階段:確認執行真正執行業務,不作任何業務檢查,只使用 Try 階段預留的業務資源,Confirm 操作要求具備冪等設計,Confirm 失敗後需要進行重試。
-
Cancel 階段:取消執行,釋放 Try 階段預留的業務資源。Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致,要求滿足冪等設計。
把上面的轉賬作為例子,通常會在Try裡面凍結金額,但不扣款,Confirm裡面扣款,Cancel裡面解凍金額,一個成功完成的TCC事務時序圖如下:
TCC特點如下:
-
併發度較高,無長期資源鎖定。
-
開發量較大,需要提供Try/Confirm/Cancel介面。
-
一致性較好,不會發生SAGA已扣款最後又轉賬失敗的情況
-
TCC適用於訂單類業務,對中間狀態有約束的業務
◆ 本地訊息表
本地訊息表這個方案最初是 ebay 架構師 Dan Pritchett 在 2008 年發表給 ACM 的文章。設計核心是將需要分散式處理的任務通過訊息的方式來非同步確保執行。
大致流程如下:
寫本地訊息和業務操作放在一個事務裡,保證了業務和發訊息的原子性,要麼他們全都成功,要麼全都失敗。
容錯機制:
-
扣減餘額事務 失敗時,事務直接回滾,無後續步驟
-
輪序生產訊息失敗, 增加餘額事務失敗都會進行重試
本地訊息表的特點:
-
長事務僅需要分拆成多個任務,使用簡單
-
生產者需要額外的建立訊息表
-
每個本地訊息表都需要進行輪詢
-
消費者的邏輯如果無法通過重試成功,那麼還需要更多的機制,來回滾操作
適用於可非同步執行的業務,且後續操作無需回滾的業務
◆ 事務訊息
在上述的本地訊息表方案中,生產者需要額外建立訊息表,還需要對本地訊息表進行輪詢,業務負擔較重。阿里開源的RocketMQ 4.3之後的版本正式支援事務訊息,該事務訊息本質上是把本地訊息表放到RocketMQ上,解決生產端的訊息傳送與本地事務執行的原子性問題。
事務訊息傳送及提交:
-
傳送訊息(half訊息)
-
服務端儲存訊息,並響應訊息的寫入結果
-
根據傳送結果執行本地事務(如果寫入失敗,此時half訊息對業務不可見,本地邏輯不執行)
-
根據本地事務狀態執行Commit或者Rollback(Commit操作釋出訊息,訊息對消費者可見)
正常傳送的流程圖如下:
補償流程:
對沒有Commit/Rollback的事務訊息(pending狀態的訊息),從服務端發起一次“回查”
Producer收到回查訊息,返回訊息對應的本地事務的狀態,為Commit或者Rollback
事務訊息方案與本地訊息表機制非常類似,區別主要在於原先相關的本地表操作替換成了一個反查介面
事務訊息特點如下:
-
長事務僅需要分拆成多個任務,並提供一個反查介面,使用簡單
-
消費者的邏輯如果無法通過重試成功,那麼還需要更多的機制,來回滾操作
適用於可非同步執行的業務,且後續操作無需回滾的業務
如果讀者想要進一步研究事務訊息,可參考rocketmq,為了方便大家學習事務訊息,DTM也提供了簡單實現
◆ 最大努力通知
發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。具體包括:
有一定的訊息重複通知機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對訊息重複通知。
訊息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費訊息後要再次消費,此時可由接收方主動向通知方查詢訊息資訊來滿足需求。
前面介紹的的本地訊息表和事務訊息都屬於可靠訊息,與這裡介紹的最大努力通知有什麼不同?
可靠訊息一致性,發起通知方需要保證將訊息發出去,並且將訊息發到接收通知方,訊息的可靠性關鍵由發起通知方來保證。
最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能訊息接收不到,此時需要接收通知方主動呼叫發起通知方的介面查詢業務處理結果,通知的可靠性關鍵在接收通知方。
解決方案上,最大努力通知需要:
-
提供介面,讓接受通知放能夠通過介面查詢業務處理結果
-
訊息佇列ACK機制,訊息佇列按照間隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知間隔 ,直到達到通知要求的時間視窗上限。之後不再通知
最大努力通知適用於業務通知型別,例如微信交易的結果,就是通過最大努力通知方式通知各個商戶,既有回撥通知,也有交易查詢介面
◆ AT事務模式
這是阿里開源專案seata中的一種事務模式,在螞蟻金服也被稱為FMT。優點是該事務模式使用方式,類似XA模式,業務無需編寫各類補償操作,回滾由框架自動完成,缺點也類似AT,存在較長時間的鎖,不滿足高併發的場景。有興趣的同學可以參考seata-AT
◆ 分散式事務中的網路異常
在分散式事務的各個環節都有可能出現網路以及業務故障等問題,這些問題需要分散式事務的業務方做到防空回滾,冪等,防懸掛三個特性,下面以TCC事務說明這些異常情況:
空回滾:
在沒有呼叫 TCC 資源 Try 方法的情況下,呼叫了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然後直接返回成功。
出現原因是當一個分支事務所在服務宕機或網路異常,分支事務呼叫記錄為失敗,這個時候其實是沒有執行Try階段,當故障恢復後,分散式事務進行回滾則會呼叫二階段的Cancel方法,從而形成空回滾。
冪等:
由於任何一個請求都可能出現網路異常,出現重複請求,所以所有的分散式事務分支,都需要保證冪等性
懸掛:
懸掛就是對於一個分散式事務,其二階段 Cancel 介面比 Try 介面先執行。
出現原因是在 RPC 呼叫分支事務try時,先註冊分支事務,再執行RPC呼叫,如果此時 RPC 呼叫的網路發生擁堵,RPC 超時以後,TM就會通知RM回滾該分散式事務,可能回滾完成後,RPC 請求才到達參與者真正執行。
下面看一個網路異常的時序圖,更好的理解上述幾種問題
業務處理請求4的時候,Cancel在Try之前執行,需要處理空回滾
業務處理請求6的時候,Cancel重複執行,需要冪等
業務處理請求8的時候,Try在Cancel後執行,需要處理懸掛
面對上述複雜的網路異常情況,目前看到各家建議的方案都是業務方通過唯一鍵,去查詢相關聯的操作是否已完成,如果已完成則直接返回成功。相關的判斷邏輯較複雜,易出錯,業務負擔重。
在專案DTM中,出現了一種子事務屏障技術,使用該技術,能夠達到這個效果,看示意圖:
所有這些請求,到了子事務屏障後:不正常的請求,會被過濾;正常請求,通過屏障。開發者使用子事務屏障之後,前面所說的各種異常全部被妥善處理,業務開發人員只需要關注實際的業務邏輯,負擔大大降低。
子事務屏障提供了方法ThroughBarrierCall,方法的原型為:
func ThroughBarrierCall(db *sql.DB, transInfo *TransInfo, busiCall BusiFunc)
業務開發人員,在busiCall裡面編寫自己的相關邏輯,呼叫該函式。ThroughBarrierCall保證,在空回滾、懸掛等場景下,busiCall不會被呼叫;在業務被重複呼叫時,有冪等控制,保證只被提交一次。
子事務屏障會管理TCC、SAGA、XA、事務訊息等,也可以擴充套件到其他領域
子事務屏障技術的原理是,在本地資料庫,建立分支事務狀態表sub_trans_barrier,唯一鍵為全域性事務id-子事務id-子事務分支名稱(try|confirm|cancel)
-
開啟事務
-
如果是Try分支,則那麼insert ignore插入gid-branchid-try,如果成功插入,則呼叫屏障內邏輯
-
如果是Confirm分支,那麼insert ignore插入gid-branchid-confirm,如果成功插入,則呼叫屏障內邏輯
-
如果是Cancel分支,那麼insert ignore插入gid-branchid-try,再插入gid-branchid-cancel,如果try未插入並且cancel插入成功,則呼叫屏障內邏輯
-
屏障內邏輯返回成功,提交事務,返回成功
-
屏障內邏輯返回錯誤,回滾事務,返回錯誤
在此機制下,解決了網路異常相關的問題
-
空補償控制--如果Try沒有執行,直接執行了Cancel,那麼Cancel插入gid-branchid-try會成功,不走屏障內的邏輯,保證了空補償控制
-
冪等控制--任何一個分支都無法重複插入唯一鍵,保證了不會重複執行
-
防懸掛控制--Try在Cancel之後執行,那麼插入的gid-branchid-try不成功,就不執行,保證了防懸掛控制
對於SAGA事務,也是類似的機制。
子事務屏障技術,為DTM首創,它的意義在於設計簡單易實現的演算法,提供了簡單易用的介面,在首創,它的意義在於設計簡單易實現的演算法,提供了簡單易用的介面,在這兩項的幫助下,開發人員徹底的從網路異常的處理中解放出來。
該技術目前需要搭配DTM事務管理器,目前SDK已經提供給go語言的開發者。其他語言的sdk正在規劃中。對於其他的分散式事務框架,只要提供了合適的分散式事務資訊,能夠按照上述原理,快速實現該技術。
--------------------------------------------------------------------------------------
事務的隔離級別
這裡擴充套件一下,對事務的隔離性做一個詳細的解釋。
在事務的四大特性ACID中,要求的隔離性是一種嚴格意義上的隔離,也就是多個事務是序列執行的,彼此之間不會受到任何干擾。這確實能夠完全保證資料的安全性,但在實際業務系統中,這種方式效能不高。因此,資料庫定義了四種隔離級別,隔離級別和資料庫的效能是呈反比的,隔離級別越低,資料庫效能越高,而隔離級別越高,資料庫效能越差。
事務併發執行會出現的問題
我們先來看一下在不同的隔離級別下,資料庫可能會出現的問題:
-
更新丟失
當有兩個併發執行的事務,更新同一行資料,那麼有可能一個事務會把另一個事務的更新覆蓋掉。
當資料庫沒有加任何鎖操作的情況下會發生。 -
髒讀
一個事務讀到另一個尚未提交的事務中的資料。
該資料可能會被回滾從而失效。
如果第一個事務拿著失效的資料去處理那就發生錯誤了。 -
不可重複讀
不可重複度的含義:一個事務對同一行資料讀了兩次,卻得到了不同的結果。它具體分為如下兩種情況:- 虛讀:在事務1兩次讀取同一記錄的過程中,事務2對該記錄進行了修改,從而事務1第二次讀到了不一樣的記錄。
- 幻讀:事務1在兩次查詢的過程中,事務2對該表進行了插入、刪除操作,從而事務1第二次查詢的結果發生了變化。
資料庫的四種隔離級別
資料庫一共有如下四種隔離級別:
-
Read uncommitted 讀未提交
在該級別下,一個事務對一行資料修改的過程中,不允許另一個事務對該行資料進行修改,但允許另一個事務對該行資料讀。
因此本級別下,不會出現更新丟失,但會出現髒讀、不可重複讀。 -
Read committed 讀提交
在該級別下,未提交的寫事務不允許其他事務訪問該行,因此不會出現髒讀;但是讀取資料的事務允許其他事務的訪問該行資料,因此會出現不可重複讀的情況。 -
Repeatable read 重複讀
在該級別下,讀事務禁止寫事務,但允許讀事務,因此不會出現同一事務兩次讀到不同的資料的情況(不可重複讀),且寫事務禁止其他一切事務。 -
Serializable 序列化
該級別要求所有事務都必須序列執行,因此能避免一切因併發引起的問題,但效率很低。
CAP理論
CAP理論說的是:在一個分散式系統中,最多隻能滿足C、A、P中的兩個需求。
CAP的含義:
- C:Consistency 一致性
同一資料的多個副本是否實時相同。 - A:Availability 可用性
可用性:一定時間內 & 系統返回一個明確的結果 則稱為該系統可用。 - P:Partition tolerance 分割槽容錯性
將同一服務分佈在多個系統中,從而保證某一個系統宕機,仍然有其他系統提供相同的服務。
CAP理論告訴我們,在分散式系統中,C、A、P三個條件中我們最多隻能選擇兩個。那麼問題來了,究竟選擇哪兩個條件較為合適呢?
對於一個業務系統來說,可用性和分割槽容錯性是必須要滿足的兩個條件,並且這兩者是相輔相成的。業務系統之所以使用分散式系統,主要原因有兩個:
-
提升整體效能
當業務量猛增,單個伺服器已經無法滿足我們的業務需求的時候,就需要使用分散式系統,使用多個節點提供相同的功能,從而整體上提升系統的效能,這就是使用分散式系統的第一個原因。 -
實現分割槽容錯性
單一節點 或 多個節點處於相同的網路環境下,那麼會存在一定的風險,萬一該機房斷電、該地區發生自然災害,那麼業務系統就全面癱瘓了。為了防止這一問題,採用分散式系統,將多個子系統分佈在不同的地域、不同的機房中,從而保證系統高可用性。
這說明分割槽容錯性是分散式系統的根本,如果分割槽容錯性不能滿足,那使用分散式系統將失去意義。
此外,可用性對業務系統也尤為重要。在大談使用者體驗的今天,如果業務系統時常出現“系統異常”、響應時間過長等情況,這使得使用者對系統的好感度大打折扣,在網際網路行業競爭激烈的今天,相同領域的競爭者不甚列舉,系統的間歇性不可用會立馬導致使用者流向競爭對手。因此,我們只能通過犧牲一致性來換取系統的可用性和分割槽容錯性。這也就是下面要介紹的BASE理論。
BASE理論
CAP理論告訴我們一個悲慘但不得不接受的事實——我們只能在C、A、P中選擇兩個條件。而對於業務系統而言,我們往往選擇犧牲一致性來換取系統的可用性和分割槽容錯性。不過這裡要指出的是,所謂的“犧牲一致性”並不是完全放棄資料一致性,而是犧牲強一致性換取弱一致性。下面來介紹下BASE理論。
- BA:Basic Available 基本可用
- 整個系統在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內仍然能夠返回一個明確的結果。只不過“基本可用”和“高可用”的區別是:
- “一定時間”可以適當延長
當舉行大促時,響應時間可以適當延長 - 給部分使用者返回一個降級頁面
給部分使用者直接返回一個降級頁面,從而緩解伺服器壓力。但要注意,返回降級頁面仍然是返回明確結果。
- “一定時間”可以適當延長
- 整個系統在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內仍然能夠返回一個明確的結果。只不過“基本可用”和“高可用”的區別是:
- S:Soft State:柔性狀態
同一資料的不同副本的狀態,可以不需要實時一致。 - E:Eventual Consisstency:最終一致性
同一資料的不同副本的狀態,可以不需要實時一致,但一定要保證經過一定時間後仍然是一致的。
分散式事務的解決方案
分散式事務的解決方案有如下幾種:
- 全域性訊息
- 基於可靠訊息服務的分散式事務
- TCC
- 最大努力通知
方案1:全域性事務(DTP模型)
全域性事務基於DTP模型實現。DTP是由X/Open組織提出的一種分散式事務模型——X/Open Distributed Transaction Processing Reference Model。它規定了要實現分散式事務,需要三種角色:
-
AP:Application 應用系統
它就是我們開發的業務系統,在我們開發的過程中,可以使用資源管理器提供的事務介面來實現分散式事務。 -
TM:Transaction Manager 事務管理器
- 分散式事務的實現由事務管理器來完成,它會提供分散式事務的操作介面供我們的業務系統呼叫。這些介面稱為TX介面。
- 事務管理器還管理著所有的資源管理器,通過它們提供的XA介面來同一排程這些資源管理器,以實現分散式事務。
- DTP只是一套實現分散式事務的規範,並沒有定義具體如何實現分散式事務,TM可以採用2PC、3PC、Paxos等協議實現分散式事務。
-
RM:Resource Manager 資源管理器
- 能夠提供資料服務的物件都可以是資源管理器,比如:資料庫、訊息中介軟體、快取等。大部分場景下,資料庫即為分散式事務中的資源管理器。
- 資源管理器能夠提供單資料庫的事務能力,它們通過XA介面,將本資料庫的提交、回滾等能力提供給事務管理器呼叫,以幫助事務管理器實現分散式的事務管理。
- XA是DTP模型定義的介面,用於向事務管理器提供該資源管理器(該資料庫)的提交、回滾等能力。
- DTP只是一套實現分散式事務的規範,RM具體的實現是由資料庫廠商來完成的。
方案2:基於可靠訊息服務的分散式事務
這種實現分散式事務的方式需要通過訊息中介軟體來實現。假設有A和B兩個系統,分別可以處理任務A和任務B。此時系統A中存在一個業務流程,需要將任務A和任務B在同一個事務中處理。下面來介紹基於訊息中介軟體來實現這種分散式事務。
- 在系統A處理任務A前,首先向訊息中介軟體傳送一條訊息
- 訊息中介軟體收到後將該條訊息持久化,但並不投遞。此時下游系統B仍然不知道該條訊息的存在。
- 訊息中介軟體持久化成功後,便向系統A返回一個確認應答;
- 系統A收到確認應答後,則可以開始處理任務A;
- 任務A處理完成後,向訊息中介軟體傳送Commit請求。該請求傳送完成後,對系統A而言,該事務的處理過程就結束了,此時它可以處理別的任務了。
但commit訊息可能會在傳輸途中丟失,從而訊息中介軟體並不會向系統B投遞這條訊息,從而系統就會出現不一致性。這個問題由訊息中介軟體的事務回查機制完成,下文會介紹。 - 訊息中介軟體收到Commit指令後,便向系統B投遞該訊息,從而觸發任務B的執行;
- 當任務B執行完成後,系統B向訊息中介軟體返回一個確認應答,告訴訊息中介軟體該訊息已經成功消費,此時,這個分散式事務完成。
上述過程可以得出如下幾個結論:
1. 訊息中介軟體扮演者分散式事務協調者的角色。
2. 系統A完成任務A後,到任務B執行完成之間,會存在一定的時間差。在這個時間差內,整個系統處於資料不一致的狀態,但這短暫的不一致性是可以接受的,因為經過短暫的時間後,系統又可以保持資料一致性,滿足BASE理論。
上述過程中,如果任務A處理失敗,那麼需要進入回滾流程,如下圖所示:
- 若系統A在處理任務A時失敗,那麼就會向訊息中介軟體傳送Rollback請求。和傳送Commit請求一樣,系統A發完之後便可以認為回滾已經完成,它便可以去做其他的事情。
- 訊息中介軟體收到回滾請求後,直接將該訊息丟棄,而不投遞給系統B,從而不會觸發系統B的任務B。
此時系統又處於一致性狀態,因為任務A和任務B都沒有執行。
上面所介紹的Commit和Rollback都屬於理想情況,但在實際系統中,Commit和Rollback指令都有可能在傳輸途中丟失。那麼當出現這種情況的時候,訊息中介軟體是如何保證資料一致性呢?——答案就是超時詢問機制。
系統A除了實現正常的業務流程外,還需提供一個事務詢問的介面,供訊息中介軟體呼叫。當訊息中介軟體收到一條事務型訊息後便開始計時,如果到了超時時間也沒收到系統A發來的Commit或Rollback指令的話,就會主動呼叫系統A提供的事務詢問介面詢問該系統目前的狀態。該介面會返回三種結果:
- 提交
若獲得的狀態是“提交”,則將該訊息投遞給系統B。 - 回滾
若獲得的狀態是“回滾”,則直接將條訊息丟棄。 - 處理中
若獲得的狀態是“處理中”,則繼續等待。
訊息中介軟體的超時詢問機制能夠防止上游系統因在傳輸過程中丟失Commit/Rollback指令而導致的系統不一致情況,而且能降低上游系統的阻塞時間,上游系統只要發出Commit/Rollback指令後便可以處理其他任務,無需等待確認應答。而Commit/Rollback指令丟失的情況通過超時詢問機制來彌補,這樣大大降低上游系統的阻塞時間,提升系統的併發度。
下面來說一說訊息投遞過程的可靠性保證。
當上遊系統執行完任務並向訊息中介軟體提交了Commit指令後,便可以處理其他任務了,此時它可以認為事務已經完成,接下來訊息中介軟體一定會保證訊息被下游系統成功消費掉!那麼這是怎麼做到的呢?這由訊息中介軟體的投遞流程來保證。
訊息中介軟體向下遊系統投遞完訊息後便進入阻塞等待狀態,下游系統便立即進行任務的處理,任務處理完成後便向訊息中介軟體返回應答。訊息中介軟體收到確認應答後便認為該事務處理完畢!
如果訊息在投遞過程中丟失,或訊息的確認應答在返回途中丟失,那麼訊息中介軟體在等待確認應答超時之後就會重新投遞,直到下游消費者返回消費成功響應為止。當然,一般訊息中介軟體可以設定訊息重試的次數和時間間隔,比如:當第一次投遞失敗後,每隔五分鐘重試一次,一共重試3次。如果重試3次之後仍然投遞失敗,那麼這條訊息就需要人工干預。
有的同學可能要問:訊息投遞失敗後為什麼不回滾訊息,而是不斷嘗試重新投遞?
這就涉及到整套分散式事務系統的實現成本問題。
我們知道,當系統A將向訊息中介軟體傳送Commit指令後,它便去做別的事情了。如果此時訊息投遞失敗,需要回滾的話,就需要讓系統A事先提供回滾介面,這無疑增加了額外的開發成本,業務系統的複雜度也將提高。對於一個業務系統的設計目標是,在保證效能的前提下,最大限度地降低系統複雜度,從而能夠降低系統的運維成本。
不知大家是否發現,上游系統A向訊息中介軟體提交Commit/Rollback訊息採用的是非同步方式,也就是當上遊系統提交完訊息後便可以去做別的事情,接下來提交、回滾就完全交給訊息中介軟體來完成,並且完全信任訊息中介軟體,認為它一定能正確地完成事務的提交或回滾。然而,訊息中介軟體向下遊系統投遞訊息的過程是同步的。也就是訊息中介軟體將訊息投遞給下游系統後,它會阻塞等待,等下游系統成功處理完任務返回確認應答後才取消阻塞等待。為什麼這兩者在設計上是不一致的呢?
首先,上游系統和訊息中介軟體之間採用非同步通訊是為了提高系統併發度。業務系統直接和使用者打交道,使用者體驗尤為重要,因此這種非同步通訊方式能夠極大程度地降低使用者等待時間。此外,非同步通訊相對於同步通訊而言,沒有了長時間的阻塞等待,因此係統的併發性也大大增加。但非同步通訊可能會引起Commit/Rollback指令丟失的問題,這就由訊息中介軟體的超時詢問機制來彌補。
那麼,訊息中介軟體和下游系統之間為什麼要採用同步通訊呢?
非同步能提升系統性能,但隨之會增加系統複雜度;而同步雖然降低系統併發度,但實現成本較低。因此,在對併發度要求不是很高的情況下,或者伺服器資源較為充裕的情況下,我們可以選擇同步來降低系統的複雜度。
我們知道,訊息中介軟體是一個獨立於業務系統的第三方中介軟體,它不和任何業務系統產生直接的耦合,它也不和使用者產生直接的關聯,它一般部署在獨立的伺服器叢集上,具有良好的可擴充套件性,所以不必太過於擔心它的效能,如果處理速度無法滿足我們的要求,可以增加機器來解決。而且,即使訊息中介軟體處理速度有一定的延遲那也是可以接受的,因為前面所介紹的BASE理論就告訴我們了,我們追求的是最終一致性,而非實時一致性,因此訊息中介軟體產生的時延導致事務短暫的不一致是可以接受的。
方案3:最大努力通知(定期校對)
最大努力通知也被稱為定期校對,其實在方案二中已經包含,這裡再單獨介紹,主要是為了知識體系的完整性。這種方案也需要訊息中介軟體的參與,其過程如下:
- 上游系統在完成任務後,向訊息中介軟體同步地傳送一條訊息,確保訊息中介軟體成功持久化這條訊息,然後上游系統可以去做別的事情了;
- 訊息中介軟體收到訊息後負責將該訊息同步投遞給相應的下游系統,並觸發下游系統的任務執行;
- 當下遊系統處理成功後,向訊息中介軟體反饋確認應答,訊息中介軟體便可以將該條訊息刪除,從而該事務完成。
上面是一個理想化的過程,但在實際場景中,往往會出現如下幾種意外情況:
- 訊息中介軟體向下遊系統投遞訊息失敗
- 上游系統向訊息中介軟體傳送訊息失敗
對於第一種情況,訊息中介軟體具有重試機制,我們可以在訊息中介軟體中設定訊息的重試次數和重試時間間隔,對於網路不穩定導致的訊息投遞失敗的情況,往往重試幾次後訊息便可以成功投遞,如果超過了重試的上限仍然投遞失敗,那麼訊息中介軟體不再投遞該訊息,而是記錄在失敗訊息表中,訊息中介軟體需要提供失敗訊息的查詢介面,下游系統會定期查詢失敗訊息,並將其消費,這就是所謂的“定期校對”。
如果重複投遞和定期校對都不能解決問題,往往是因為下游系統出現了嚴重的錯誤,此時就需要人工干預。
對於第二種情況,需要在上游系統中建立訊息重發機制。可以在上游系統建立一張本地訊息表,並將 任務處理過程 和 向本地訊息表中插入訊息 這兩個步驟放在一個本地事務中完成。如果向本地訊息表插入訊息失敗,那麼就會觸發回滾,之前的任務處理結果就會被取消。如果這量步都執行成功,那麼該本地事務就完成了。接下來會有一個專門的訊息傳送者不斷地傳送本地訊息表中的訊息,如果傳送失敗它會返回重試。當然,也要給訊息傳送者設定重試的上限,一般而言,達到重試上限仍然傳送失敗,那就意味著訊息中介軟體出現嚴重的問題,此時也只有人工干預才能解決問題。
對於不支援事務型訊息的訊息中介軟體,如果要實現分散式事務的話,就可以採用這種方式。它能夠通過重試機制+定期校對實現分散式事務,但相比於第二種方案,它達到資料一致性的週期較長,而且還需要在上游系統中實現訊息重試釋出機制,以確保訊息成功釋出給訊息中介軟體,這無疑增加了業務系統的開發成本,使得業務系統不夠純粹,並且這些額外的業務邏輯無疑會佔用業務系統的硬體資源,從而影響效能。
因此,儘量選擇支援事務型訊息的訊息中介軟體來實現分散式事務,如RocketMQ。
方案4:TCC(兩階段型、補償型)
TCC即為Try Confirm Cancel,它屬於補償型分散式事務。顧名思義,TCC實現分散式事務一共有三個步驟:
- Try:嘗試待執行的業務
- 這個過程並未執行業務,只是完成所有業務的一致性檢查,並預留好執行所需的全部資源
- Confirm:執行業務
- 這個過程真正開始執行業務,由於Try階段已經完成了一致性檢查,因此本過程直接執行,而不做任何檢查。並且在執行的過程中,會使用到Try階段預留的業務資源。
- Cancel:取消執行的業務
- 若業務執行失敗,則進入Cancel階段,它會釋放所有佔用的業務資源,並回滾Confirm階段執行的操作。
下面以一個轉賬的例子來解釋下TCC實現分散式事務的過程。
假設使用者A用他的賬戶餘額給使用者B發一個100元的紅包,並且餘額系統和紅包系統是兩個獨立的系統。
-
Try
- 建立一條轉賬流水,並將流水的狀態設為交易中
- 將使用者A的賬戶中扣除100元(預留業務資源)
- Try成功之後,便進入Confirm階段
- Try過程發生任何異常,均進入Cancel階段
-
Confirm
- 向B使用者的紅包賬戶中增加100元
- 將流水的狀態設為交易已完成
- Confirm過程發生任何異常,均進入Cancel階段
- Confirm過程執行成功,則該事務結束
-
Cancel
- 將使用者A的賬戶增加100元
- 將流水的狀態設為交易失敗
在傳統事務機制中,業務邏輯的執行和事務的處理,是在不同的階段由不同的部件來完成的:業務邏輯部分訪問資源實現資料儲存,其處理是由業務系統負責;事務處理部分通過協調資源管理器以實現事務管理,其處理由事務管理器來負責。二者沒有太多互動的地方,所以,傳統事務管理器的事務處理邏輯,僅需要著眼於事務完成(commit/rollback)階段,而不必關注業務執行階段。
TCC全域性事務必須基於RM本地事務來實現全域性事務
TCC服務是由Try/Confirm/Cancel業務構成的,
其Try/Confirm/Cancel業務在執行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取資料。這些存取操作,必須要參與RM本地事務,以使其更改的資料要麼都commit,要麼都rollback。
這一點不難理解,考慮一下如下場景:
假設圖中的服務B沒有基於RM本地事務(以RDBS為例,可通過設定auto-commit為true來模擬),那麼一旦[B:Try]操作中途執行失敗,TCC事務框架後續決定回滾全域性事務時,該[B:Cancel]則需要判斷[B:Try]中哪些操作已經寫到DB、哪些操作還沒有寫到DB:假設[B:Try]業務有5個寫庫操作,[B:Cancel]業務則需要逐個判斷這5個操作是否生效,並將生效的操作執行反向操作。
不幸的是,由於[B:Cancel]業務也有n(0<=n<=5)個反向的寫庫操作,此時一旦[B:Cancel]也中途出錯,則後續的[B:Cancel]執行任務更加繁重。因為,相比第一次[B:Cancel]操作,後續的[B:Cancel]操作還需要判斷先前的[B:Cancel]操作的n(0<=n<=5)個寫庫中哪幾個已經執行、哪幾個還沒有執行,這就涉及到了冪等性問題。而對冪等性的保障,又很可能還需要涉及額外的寫庫操作,該寫庫操作又會因為沒有RM本地事務的支援而存在類似問題。。。可想而知,如果不基於RM本地事務,TCC事務框架是無法有效的管理TCC全域性事務的。
反之,基於RM本地事務的TCC事務,這種情況則會很容易處理:[B:Try]操作中途執行失敗,TCC事務框架將其參與RM本地事務直接rollback即可。後續TCC事務框架決定回滾全域性事務時,在知道“[B:Try]操作涉及的RM本地事務已經rollback”的情況下,根本無需執行[B:Cancel]操作。
換句話說,基於RM本地事務實現TCC事務框架時,一個TCC型服務的cancel業務要麼執行,要麼不執行,不需要考慮部分執行的情況。
TCC事務框架應該提供Confirm/Cancel服務的冪等性保障
一般認為,服務的冪等性,是指標對同一個服務的多次(n>1)請求和對它的單次(n=1)請求,二者具有相同的副作用。
在TCC事務模型中,Confirm/Cancel業務可能會被重複呼叫,其原因很多。比如,全域性事務在提交/回滾時會呼叫各TCC服務的Confirm/Cancel業務邏輯。執行這些Confirm/Cancel業務時,可能會出現如網路中斷的故障而使得全域性事務不能完成。因此,故障恢復機制後續仍然會重新提交/回滾這些未完成的全域性事務,這樣就會再次呼叫參與該全域性事務的各TCC服務的Confirm/Cancel業務邏輯。
既然Confirm/Cancel業務可能會被多次呼叫,就需要保障其冪等性。
那麼,應該由TCC事務框架來提供冪等性保障?還是應該由業務系統自行來保障冪等性呢?
個人認為,應該是由TCC事務框架來提供冪等性保障。如果僅僅只是極個別服務存在這個問題的話,那麼由業務系統來負責也是可以的;然而,這是一類公共問題,毫無疑問,所有TCC服務的Confirm/Cancel業務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決;而且,考慮一下由業務系統來負責冪等性需要考慮的問題,就會發現,這無疑增大了業務系統的複雜度。