分散式事務架構的五大演進 ,以交易系統為例
作者介紹
樑陽鶴,樂視網BOSS平臺技術部架構師,主要負責樂視集團支付、樂視會員系統、商業運營平臺等系統架構工作。開源資料訪問層框架Mango作者。
一、概述
在支付、交易、訂單等強一致性系統中,我們需要使用分散式事務來保證各個資料庫或各個系統之間的資料一致性。
舉個簡單的例子來描述一下這裡資料一致性的含義。
程式設計師小張向女友小麗轉賬100人民幣,轉賬過程是:先扣除小張100元,再為小麗的賬戶新增100元。
如果在轉帳過程中,扣款操作和打款操作要麼同時執行,要麼同時都不執行,我們就認為轉帳過程保證了資料一致性。
上面的例子中,如果我們不使用分散式來保證轉賬過程中資料的一致性,就有可能出現小張賬戶上的錢被扣除,而小麗賬戶上的錢卻沒被新增的情況,其結果大家可以自行腦補。
事務是資料庫特有的概念,分散式事務最初起源於處理多個數據庫之間的資料一致性問題,但隨著IT技術的高速發展,大型系統中逐漸使用SOA服務化介面替換直接對資料庫操作,所以如何保證各個SOA服務之間的資料一致性也被劃分到分散式事務的範疇。
本文將從一個最為簡單的交易系統出發,由淺入深地講述分散式事務架構的演進過程,希望對大家理解分散式事物架構有所幫助。
二、單資料庫事務
先來看看我們需要實現的交易系統:遊戲中的玩家使用金幣購買道具,交易系統需要負責扣除玩家金幣併為玩家新增道具。
我們把交易系統的一次交易流程歸納為兩步:
-
扣除玩家金幣
-
為玩家新增道具
需求並不複雜,我們為金幣系統在資料庫中新增金幣表,為道具系統在資料庫中新增道具表,扣除金幣與新增道具的操作只需執行相應的SQL即可。
這裡我們假設金幣表與道具表都在同一個資料庫中,於是可以簡單地使用單資料庫事務來保證資料的一致性。
下面是使用單資料庫事務進行一次正常交易的時序圖:
上圖演示了一次正常交易的流程,一般情況下正常的交易流程不會產生資料不一致問題。
下面討論當出現異常時,如何使用單資料庫事務保證資料一致性:
-
在步驟[2]執行SQL扣除金幣時出現異常,回滾事務即可保證資料一致;
-
在步驟[4]執行SQL新增道具時出現異常,回滾事務即可保證資料一致;
-
在步驟[6]提交事務時出現異常,回滾事務即可保證資料一致。
通過上面三種異常的處理方式,我們不難看出,其實使用單資料庫事務保證資料一致性特別簡單,只需沒有異常提交事務而出現異常回滾事務即可。
三、基於後置提交的多資料庫事務
隨著玩家數量激增,金幣表與道具表的總行數與訪問量都急劇擴大,單臺數據庫不足以支撐起這兩張表的讀寫請求,這時將金幣表與道具表放在不同的資料庫中是個不錯的選擇。
這裡我們假設金幣表被放入了金幣資料庫中,而道具表被放入了道具資料庫中,通常我們將這種按不同業務拆分資料庫的方式稱之為資料庫垂直拆分。
資料庫垂直拆分能大大緩解資料庫的壓力問題,但多個數據庫的存在意味著我們不能通過簡單的單資料庫事務來保證資料的一致性,如何保證多資料庫之間資料的一致性,也就是分散式事務需要解決的問題。
回到我們的交易系統,先不考慮多資料庫之間的資料一致性問題,簡單的交易流程為:
正常情況下,上面的流程不會產生資料一致性問題,但如果在步驟[7]執行SQL新增道具時出現異常,由於扣除金幣的事務已經在步驟[5]提交無法回滾,就會出現扣除玩家金幣後沒有為玩家新增道具的資料不一致情況。
上面問題產生的原因其實是過早地向金幣資料庫提交事務,所以我們可以採取後置提交事務策略來解決此問題,即先在金幣資料庫與道具資料庫上執行SQL,最後再提交金幣資料庫與道具資料庫上的事務,這樣當執行SQL出現異常時,我們就能通過同時回滾兩個資料庫上事務的方式,來保證資料一致性。
下面是使用後置提交事務進行一次正常交易的時序圖:
結合上圖,我們討論當出現異常時,後置提交事務如何避免資料不一致問題:
-
在步驟[3]執行SQL扣除金幣時出現異常,回滾金幣資料庫上的事務即可保證資料一致;
-
在步驟[5]執行SQL新增道具時出現異常,同時回滾金幣資料庫與道具資料庫上的事務即可保證資料一致;
-
在步驟[7]提交扣除金幣事務時出現異常,同時回滾金幣資料庫與道具資料庫上的事務即可保證資料一致;
-
在步驟[9]提交新增道具事務時出現異常,由於扣除金幣事務已提交無法回滾,會出現扣除玩家金幣後沒有為玩家新增道具的資料不一致情況。
通過上面四種異常的處理方式,我們可以看出,使用後置提交事務的策略,雖然能避免SQL執行異常導致的資料不一致,但在最後提交事務遇到異常時卻無能為力,所以我們需要引入新的事務提交方式。
四、兩段式事務
如前面所述,將不同資料庫上的事務放在最後一起提交能解決SQL執行異常導致的資料不一致問題,但如若在最後提交事務時,前面的事務提交成功,最後的事務提交失敗,由於那些已經提交成功的事務無法回滾,同樣會產生資料不一致問題。
由於傳統的事務提交機制無法保證多個數據庫之間的資料一致性,於是電腦科學家們引入了兩段式事務。
兩段式事務將事務提交操作拆分成了兩步:prepare預提交與commit確認提交。
在兩段式事務中,預提交是一個很“重”的操作,他幾乎執行了整個事務提交的所有操作,而最後的確認提交則是一個很“輕”的操作,用於最終確認事務完成。
假設我們有A、B、C共3臺數據庫,下面我們使用後置事務提交策略與兩段式事務來實現跨A、B、C資料庫的分散式事務:
-
分別獲取到A、B、C資料庫的連線並開啟事物;
-
分別在A、B、C資料庫上執行SQL;
-
分別在A、B、C資料庫上執行事務預提交;
-
分別在A、B、C資料庫上執行事務確認提交。
如上面討論,步驟3事務預提交處理了整個事務提交的大部分操作,所以一般情況下,如果步驟3事務預提交執行成功,我們可以認為步驟4事務確認提交一定會執行成功,而如果在步驟3事務預提交過程中出現異常,我們則只需回滾所有事務即可保證資料的一致性。
當然在極端情況下,會出現在步驟3事務預提交成功,而在步驟4事務確認提交失敗的情況,不過這種情況發生的概率極低,我們可以先記錄錯誤日誌,後續使用定時任務修復資料或直接人工修復資料。
我們將購買道具的交易流程改為兩段提交,時序圖如下:
其實上面的兩段式事務也就是著名的XA事務,XA是由X/Open組織提出的分散式事務的規範,也是使用最為廣泛的多資料庫分散式事務規範,目前市面上主流的資料庫MySQL,Oralce,SQLServer等都支援XA事務。
一般情況下,我們在使用XA規範編寫多資料庫分散式事務程式碼時,不用自己去實現兩段提交程式碼,而是使用atomikos等開源的分散式事務工具。
下面是一個使用atomikos實現簡單分散式事務(XA事務)的原始碼:
github.com/liangyanghe/xa-transaction-demo
五、TCC事務
之前我們的交易系統在進行購買道具時,都是直接操作金幣表與道具表,下面我們對交易系統的架構進行升級:
將與金幣相關的操作獨立成一套金幣服務,將與道具相關的操作獨立成一套道具服務,交易系統在扣除金幣與新增道具時,不再直接操作資料庫表,而是呼叫相應服務的SOA介面。
基於SOA介面的最簡交易時序圖如下:
上圖中,我們的交易系統不再直接操作資料庫表,而是通過呼叫SOA介面的方式扣除金幣與新增道具。
我們考慮在步驟[3]呼叫SOA介面新增道具時出現異常,由於之前已經呼叫SOA介面扣除金幣成功,於是就會出現扣除玩家金幣後,沒有為玩家新增道具的不一致情況。
為保證各個SOA服務之間的資料一致性,我們需要設計基於SOA介面的分散式事務。
目前比較流行的SOA分散式事務解決方案是TCC事務,TCC事務的全稱為:Try-Confirm/Cancel,翻譯成中文即:嘗試、確定、取消。
簡單來說,TCC事務是一種程式設計模式,如果SOA介面的提供者與呼叫者都遵從TCC程式設計模式,那麼就能最大限度的保證資料一致性。
下面我們以扣除金幣這一操作,來說明一下TCC程式設計模式。
非TCC模式的扣除金幣操作,介面提供者只需要提供一個SOA介面即可,介面的作用就是扣除金幣。
而TCC模式的扣除金幣操作,介面提供者針對扣除金幣這一操作需要提供三個SOA介面:
-
扣除金幣Try介面,嘗試扣除金幣,這裡只是鎖定玩家賬戶中需要被扣除的金幣,並沒有真正扣除金幣,類似於信用卡的預授權;假設玩家賬戶中100金幣,呼叫該介面鎖定60金幣後,鎖定的金幣不能再被使用,玩家賬戶中還有40金幣可用
-
扣除金幣Confirm介面,確定扣除金幣,這裡將真正扣除玩家賬戶中被鎖定的金幣,類似於信用卡的確定預授權完成刷卡
-
扣除金幣Cancel介面,取消扣除金幣,被鎖定的金幣將返還到玩家的賬戶中,類似於信用卡的撤銷預授權取消刷卡
SOA介面呼叫者如何使用這三個介面呢?
呼叫者先執行扣除金幣Try介面,再去執行其他任務(比如新增道具),當其他任務執行成功,呼叫者執行扣除金幣Confirm介面確認扣除金幣,而當其他任務執行異常,呼叫者則執行扣除金幣Cancel介面取消扣除金幣。
這裡我們假設新增道具的SOA介面也滿足TCC模式,下圖是使用TCC事務進行道具購買的時序圖:
對照上圖,我們分析一下TCC事務如何在各種異常情況下,保證資料的一致性:
-
在步驟[1]呼叫扣除金幣Try介面時出現異常,呼叫扣除金幣Cancel介面即可保證資料一致
-
在步驟[3]呼叫新增道具Try介面時出現異常,呼叫扣除金幣Cancel介面與新增道具Cancel介面即可保證資料一致
-
在步驟[5]呼叫扣除金幣Confirm介面時出現異常,呼叫扣除金幣Cancel介面與新增道具Cancel介面即可保證資料一致
-
在步驟[7]呼叫新增道具Confirm介面時出現異常,由於扣除金幣操作已經確定不能再取消,所以這裡會引發資料不一致
通過上面四種異常,我們可以看出,即使我們使用了TCC事務,也無法完美的保證各個SOA服務之間的資料一致性。
但TCC事務為我們遮蔽了大多數異常導致的資料不一致,同時一般情況下,進行Confirm或Cancel操作時產生異常的概率極小極小,所以對於一些強一致性系統,我們還是會使用TCC事務來保證多個SOA服務之間的資料一致性。
六、最終一致性
有了TCC事務,我們能夠保證多個SOA服務之間的資料一致性,但細心的朋友可能已經發現,TCC事務存在不小的效能問題。
為了描述效能問題的產生,我們將交易系統的需求略作修改:遊戲中的玩家使用金幣購買道具A,系統將自動贈送給玩家道具B,道具C與道具D。
這裡我們假設我們到道具服務不支援批量新增道具,而只有基於TCC模式的新增單個道具的介面。
為保證資料一致性,交易系統需要先呼叫扣除金幣Try介面,然後再依次呼叫新增道具A、B、C、D的Try介面,最後再依次呼叫對應的Confirm介面。
由於TCC事務是先Try再Confirm的模式,介面呼叫量會翻倍,這在介面呼叫量小時效能影響並不明顯,但上面的需求中我們執行扣除金幣,新增道具A、B、C、D共有5個介面呼叫,翻倍後變為10個,系統性能會大大降低。
那麼是否有既能保證資料一致性,又能保證效能的分散式事務方案?
在回答這個問題之前,我們先將事務一致性劃分為兩類:
-
強一致性事務,請求結束後,資料就已經一致
-
最終一致性事務,請求結束後,資料沒有一致,但一段時間後資料能保持一致
其實我們使用的基於後置提交的多資料庫事務與TCC事務都屬於強一致性事務,使用強一致性事務能保證事務的實時性,但卻很難在高併發環境中保證效能。
再來看最終一致性事務,最終一致性事務這幾個字看起來很牛逼,但說白了就是非同步資料補償,即在核心流程我們只保證核心資料的實時資料一致性,對於非核心資料,我們通過非同步程式來保證資料一致性。
由於最終一致性事務引入了非同步資料補償機制,主流程的執行流程被簡化,效能自然得到提高。
目前主流觸發非同步資料補償的方式有兩種:
-
使用訊息佇列實時觸發資料補償,核心流程在保證核心資料的一致性後,使用訊息佇列的方式通知非同步程式進行資料補償,這種方式能近乎實時的使資料達到最終一致性,但如果訊息佇列或非同步程式出現異常,資料一致性也將不能保證
-
使用定時任務週期性觸發資料補償,核心流程在保證核心資料的一致性後直接返回,由定時任務週期性觸發資料補償程式,這種方式雖不能像訊息佇列那樣能近乎實時的使資料達到最終一致性,但資料補償程式出現異常時,我們能比較容易在下個週期對資料進行修復,能最大限度的保證資料的一致性
上面兩種非同步資料補償的方式各有利弊,訊息佇列方式實時性強,但在異常情況下一致性弱,而定時任務方式實時性弱,但在異常情況下一致性強。
其實最優的策略是同時使用訊息佇列與定時任務觸發資料補償。
正常情況下,我們使用訊息佇列近乎實時的非同步觸發資料補償,而針對那些極少發生的異常,我們使用定時任務週期性的修補資料。
這樣在正常情況下,我們能近乎實時的使資料達到最終一致性,而對於一些異常資料則按照定時任務的執行週期,週期性的達到最終一致性。
回到上面的新版交易系統:遊戲中的玩家使用金幣購買道具A,系統將自動贈送給玩家道具B,道具C與道具D。
下圖是使用訊息佇列實時觸發資料補償實現最終一致性的時序圖(如看不清楚可以點選圖片放大):
上圖中,我們使用TCC事務保證了扣除金幣與新增道具A資料一致,然後傳送贈送訊息並結束請求,贈送系統收到訊息後負責新增道具B、C、D,最終保證資料一致。
這裡如果訊息佇列或贈送服務出現異常我們的最終一致性將難以保證,所以我們可以再引入一個定時任務,週期性的觸發異常資料補償。
這樣我們就實現了一個既能保證最終資料一致,又能保證效能的道具買贈系統。