1、概述git
在支付、交易、訂單等強一致性系統中,咱們須要使用分佈式事務來保證各個數據庫或各個系統之間的數據一致性。程序員
舉個簡單的例子來描述一下這裏數據一致性的含義。github
程序員小張向女朋友小麗轉帳100人民幣,轉帳過程是:先扣除小張100元,再爲小麗的帳戶添加100元。數據庫
若是在轉賬過程當中,扣款操做和打款操做要麼同時執行,要麼同時都不執行,咱們就認爲轉賬過程保證了數據一致性。編程
上面的例子中,若是咱們不使用分佈式來保證轉帳過程當中數據的一致性,就有可能出現小張帳戶上的錢被扣除,而小麗帳戶上的錢卻沒被添加的狀況,其結果你們能夠自行腦補。架構
事務是數據庫特有的概念,分佈式事務最初起源於處理多個數據庫之間的數據一致性問題,但隨着IT技術的高速發展,大型系統中逐漸使用SOA服務化接口替換直接對數據庫操做,因此如何保證各個SOA服務之間的數據一致性也被劃分到分佈式事務的範疇。併發
本文將從一個最爲簡單的交易系統出發,由淺入深地講述分佈式事務架構的演進過程,但願對你們理解分佈式事物架構有所幫助。異步
2、單數據庫事務分佈式
先來看看咱們須要實現的交易系統:遊戲中的玩家使用金幣購買道具,交易系統須要負責扣除玩家金幣併爲玩家添加道具。高併發
咱們把交易系統的一次交易流程概括爲兩步:
扣除玩家金幣
爲玩家添加道具
需求並不複雜,咱們爲金幣系統在數據庫中添加金幣表,爲道具系統在數據庫中添加道具表,扣除金幣與添加道具的操做只需執行相應的SQL便可。
這裏咱們假設金幣表與道具表都在同一個數據庫中,因而能夠簡單地使用單數據庫事務來保證數據的一致性。
下面是使用單數據庫事務進行一次正常交易的時序圖:
上圖演示了一次正常交易的流程,通常狀況下正常的交易流程不會產生數據不一致問題。
下面討論當出現異常時,如何使用單數據庫事務保證數據一致性:
在步驟[2]執行SQL扣除金幣時出現異常,回滾事務便可保證數據一致;
在步驟[4]執行SQL添加道具時出現異常,回滾事務便可保證數據一致;
在步驟[6]提交事務時出現異常,回滾事務便可保證數據一致。
經過上面三種異常的處理方式,咱們不難看出,其實使用單數據庫事務保證數據一致性特別簡單,只需沒有異常提交事務而出現異常回滾事務便可。
3、基於後置提交的多數據庫事務
隨着玩家數量激增,金幣表與道具表的總行數與訪問量都急劇擴大,單臺數據庫不足以支撐起這兩張表的讀寫請求,這時將金幣表與道具表放在不一樣的數據庫中是個不錯的選擇。
這裏咱們假設金幣表被放入了金幣數據庫中,而道具表被放入了道具數據庫中,一般咱們將這種按不一樣業務拆分數據庫的方式稱之爲數據庫垂直拆分。
數據庫垂直拆分能大大緩解數據庫的壓力問題,但多個數據庫的存在乎味着咱們不能經過簡單的單數據庫事務來保證數據的一致性,如何保證多數據庫之間數據的一致性,也就是分佈式事務須要解決的問題。
回到咱們的交易系統,先不考慮多數據庫之間的數據一致性問題,簡單的交易流程爲:
正常狀況下,上面的流程不會產生數據一致性問題,但若是在步驟[7]執行SQL添加道具時出現異常,因爲扣除金幣的事務已經在步驟[5]提交沒法回滾,就會出現扣除玩家金幣後沒有爲玩家添加道具的數據不一致狀況。
上面問題產生的緣由實際上是過早地向金幣數據庫提交事務,因此咱們能夠採起後置提交事務策略來解決此問題,即先在金幣數據庫與道具數據庫上執行SQL,最後再提交金幣數據庫與道具數據庫上的事務,這樣當執行SQL出現異常時,咱們就能經過同時回滾兩個數據庫上事務的方式,來保證數據一致性。
下面是使用後置提交事務進行一次正常交易的時序圖:
結合上圖,咱們討論當出現異常時,後置提交事務如何避免數據不一致問題:
在步驟[3]執行SQL扣除金幣時出現異常,回滾金幣數據庫上的事務便可保證數據一致;
在步驟[5]執行SQL添加道具時出現異常,同時回滾金幣數據庫與道具數據庫上的事務便可保證數據一致;
在步驟[7]提交扣除金幣事務時出現異常,同時回滾金幣數據庫與道具數據庫上的事務便可保證數據一致;
在步驟[9]提交添加道具事務時出現異常,因爲扣除金幣事務已提交沒法回滾,會出現扣除玩家金幣後沒有爲玩家添加道具的數據不一致狀況。
經過上面四種異常的處理方式,咱們能夠看出,使用後置提交事務的策略,雖然能避免SQL執行異常致使的數據不一致,但在最後提交事務遇到異常時卻無能爲力,因此咱們須要引入新的事務提交方式。
4、兩段式事務
如前面所述,將不一樣數據庫上的事務放在最後一塊兒提交能解決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
5、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服務之間的數據一致性。
6、最終一致性
有了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,最終保證數據一致。
這裏若是消息隊列或贈送服務出現異常咱們的最終一致性將難以保證,因此咱們能夠再引入一個定時任務,週期性的觸發異常數據補償。
這樣咱們就實現了一個既能保證最終數據一致,又能保證性能的道具買贈系統。