一文讀懂分布式事務一致性


轉載自:https://blog.csdn.net/w05980598/article/details/79305239

一、概述

在支付、交易、訂單等強一致性系統中,我們需要使用分布式事務來保證各個數據庫或各個系統之間的數據一致性。

舉個簡單的例子來描述一下這里數據一致性的含義。 

程序員小張向女友小麗轉賬100人民幣,轉賬過程是:先扣除小張100元,再為小麗的賬戶添加100元。

如果在轉帳過程中,扣款操作和打款操作要么同時執行,要么同時都不執行,我們就認為轉帳過程保證了數據一致性。

上面的例子中,如果我們不使用分布式來保證轉賬過程中數據的一致性,就有可能出現小張賬戶上的錢被扣除,而小麗賬戶上的錢卻沒被添加的情況,其結果大家可以自行腦補。

事務是數據庫特有的概念,分布式事務最初起源於處理多個數據庫之間的數據一致性問題,但隨着IT技術的高速發展,大型系統中逐漸使用SOA服務化接口替換直接對數據庫操作,所以如何保證各個SOA服務之間的數據一致性也被划分到分布式事務的范疇。 

本文將從一個最為簡單的交易系統出發,由淺入深地講述分布式事務架構的演進過程,希望對大家理解分布式事物架構有所幫助。

 

二、單數據庫事務

先來看看我們需要實現的交易系統:游戲中的玩家使用金幣購買道具,交易系統需要負責扣除玩家金幣並為玩家添加道具。

我們把交易系統的一次交易流程歸納為兩步:

  1. 扣除玩家金幣

  2. 為玩家添加道具

需求並不復雜,我們為金幣系統在數據庫中添加金幣表,為道具系統在數據庫中添加道具表,扣除金幣與添加道具的操作只需執行相應的SQL即可。

這里我們假設金幣表與道具表都在同一個數據庫中,於是可以簡單地使用單數據庫事務來保證數據的一致性。

下面是使用單數據庫事務進行一次正常交易的時序圖:

 上圖演示了一次正常交易的流程,一般情況下正常的交易流程不會產生數據不一致問題。

下面討論當出現異常時,如何使用單數據庫事務保證數據一致性: 

  1. 在步驟[2]執行SQL扣除金幣時出現異常,回滾事務即可保證數據一致

  2. 在步驟[4]執行SQL添加道具時出現異常,回滾事務即可保證數據一致

  3. 在步驟[6]提交事務時出現異常,回滾事務即可保證數據一致

通過上面三種異常的處理方式,我們不難看出,其實使用單數據庫事務保證數據一致性特別簡單,只需沒有異常提交事務而出現異常回滾事務即可。

三、基於后置提交的多數據庫事務

 

隨着玩家數量激增,金幣表與道具表的總行數與訪問量都急劇擴大,單台數據庫不足以支撐起這兩張表的讀寫請求,這時將金幣表與道具表放在不同的數據庫中是個不錯的選擇。 

這里我們假設金幣表被放入了金幣數據庫中,而道具表被放入了道具數據庫中,通常我們將這種按不同業務拆分數據庫的方式稱之為數據庫垂直拆分。 

數據庫垂直拆分能大大緩解數據庫的壓力問題,但多個數據庫的存在意味着我們不能通過簡單的單數據庫事務來保證數據的一致性,如何保證多數據庫之間數據的一致性,也就是分布式事務需要解決的問題。

回到我們的交易系統,先不考慮多數據庫之間的數據一致性問題,簡單的交易流程為:

 正常情況下,上面的流程不會產生數據一致性問題,但如果在步驟[7]執行SQL添加道具時出現異常,由於扣除金幣的事務已經在步驟[5]提交無法回滾,就會出現扣除玩家金幣后沒有為玩家添加道具的數據不一致情況。

上面問題產生的原因其實是過早地向金幣數據庫提交事務,所以我們可以采取后置提交事務策略來解決此問題,即先在金幣數據庫與道具數據庫上執行SQL,最后再提交金幣數據庫與道具數據庫上的事務,這樣當執行SQL出現異常時,我們就能通過同時回滾兩個數據庫上事務的方式,來保證數據一致性。

下面是使用后置提交事務進行一次正常交易的時序圖:

 結合上圖,我們討論當出現異常時,后置提交事務如何避免數據不一致問題:

  1. 在步驟[3]執行SQL扣除金幣時出現異常,回滾金幣數據庫上的事務即可保證數據一致

  2. 在步驟[5]執行SQL添加道具時出現異常,同時回滾金幣數據庫與道具數據庫上的事務即可保證數據一致

  3. 在步驟[7]提交扣除金幣事務時出現異常,同時回滾金幣數據庫與道具數據庫上的事務即可保證數據一致

  4. 在步驟[9]提交添加道具事務時出現異常,由於扣除金幣事務已提交無法回滾,會出現扣除玩家金幣后沒有為玩家添加道具的數據不一致情況

通過上面四種異常的處理方式,我們可以看出,使用后置提交事務的策略,雖然能避免SQL執行異常導致的數據不一致,但在最后提交事務遇到異常時卻無能為力,所以我們需要引入新的事務提交方式。

四、基於兩段提交的多數據事務

由於傳統方式的事務提交無法完美保證多個數據庫之間數據的一致,於是計算機科學家們引入了兩段式事務提交(這是目前多數據庫分布式事務的通用解決方案)。

所謂兩段式事務提交指的是在執行提交commit操作前,添加預提交prepare操作,其中預提交操作執行了傳統意義上提交操作的大部分工作,我們可以簡單地認為只要預提交prepare操作成功,后續的提交commit操作一定會成功。

我們將購買道具的交易流程改為兩段提交,時序圖如下:

 上圖中,我們在真正提交事務之前采用了預提交事務,預提交是一個很重的操作,一旦拋出異常,我們回滾事務即可,但預提交一旦成功,后續的提交操作則非常輕量,幾乎可以認為不會出錯,這樣我們就使用兩段提交保證了多個數據庫之間數據的一致性。

其實上面的兩段式事務也就是著名的XA事務,XA是由X/Open組織提出的分布式事務的規范,也是目前使用最為廣泛的多數據庫分布式事務規范。

一般情況下,我們在使用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接口:

  1. 扣除金幣Try接口,嘗試扣除金幣,這里只是鎖定玩家賬戶中需要被扣除的金幣,並沒有真正扣除金幣,類似於信用卡的預授權;假設玩家賬戶中100金幣,調用該接口鎖定60金幣后,鎖定的金幣不能再被使用,玩家賬戶中還有40金幣可用

  2. 扣除金幣Confirm接口,確定扣除金幣,這里將真正扣除玩家賬戶中被鎖定的金幣,類似於信用卡的確定預授權完成刷卡

  3. 扣除金幣Cancel接口,取消扣除金幣,被鎖定的金幣將返還到玩家的賬戶中,類似於信用卡的撤銷預授權取消刷卡

SOA接口調用者如何使用這三個接口呢?

調用者先執行扣除金幣Try接口,再去執行其他任務(比如添加道具),當其他任務執行成功,調用者執行扣除金幣Confirm接口確認扣除金幣,而當其他任務執行異常,調用者則執行扣除金幣Cancel接口取消扣除金幣。

這里我們假設添加道具的SOA接口也滿足TCC模式,下圖是使用TCC事務進行道具購買的時序圖:

 對照上圖,我們分析一下TCC事務如何在各種異常情況下,保證數據的一致性:

  1. 在步驟[1]調用扣除金幣Try接口時出現異常,調用扣除金幣Cancel接口即可保證數據一致

  2. 在步驟[3]調用添加道具Try接口時出現異常,調用扣除金幣Cancel接口與添加道具Cancel接口即可保證數據一致

  3. 在步驟[5]調用扣除金幣Confirm接口時出現異常,調用扣除金幣Cancel接口與添加道具Cancel接口即可保證數據一致

  4. 在步驟[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個,系統性能會大大降低。

那么是否有既能保證數據一致性,又能保證性能的分布式事務方案?

在回答這個問題之前,我們先將事務一致性划分為兩類:

  1. 強一致性事務,請求結束后,數據就已經一致

  2. 最終一致性事務,請求結束后,數據沒有一致,但一段時間后數據能保持一致

 

其實我們使用的基於后置提交的多數據庫事務與TCC事務都屬於強一致性事務,使用強一致性事務能保證事務的實時性,但卻很難在高並發環境中保證性能。

再來看最終一致性事務,最終一致性事務這幾個字看起來很牛逼,但說白了就是異步數據補償,即在核心流程我們只保證核心數據的實時數據一致性,對於非核心數據,我們通過異步程序來保證數據一致性。

由於最終一致性事務引入了異步數據補償機制,主流程的執行流程被簡化,性能自然得到提高。

目前主流觸發異步數據補償的方式有兩種:

  1. 使用消息隊列實時觸發數據補償,核心流程在保證核心數據的一致性后,使用消息隊列的方式通知異步程序進行數據補償,這種方式能近乎實時的使數據達到最終一致性,但如果消息隊列或異步程序出現異常,數據一致性也將不能保證

  2. 使用定時任務周期性觸發數據補償,核心流程在保證核心數據的一致性后直接返回,由定時任務周期性觸發數據補償程序,這種方式雖不能像消息隊列那樣能近乎實時的使數據達到最終一致性,但數據補償程序出現異常時,我們能比較容易在下個周期對數據進行修復,能最大限度的保證數據的一致性

 

上面兩種異步數據補償的方式各有利弊,消息隊列方式實時性強,但在異常情況下一致性弱,而定時任務方式實時性弱,但在異常情況下一致性強。

其實最優的策略是同時使用消息隊列與定時任務觸發數據補償。

正常情況下,我們使用消息隊列近乎實時的異步觸發數據補償,而針對那些極少發生的異常,我們使用定時任務周期性的修補數據。

這樣在正常情況下,我們能近乎實時的使數據達到最終一致性,而對於一些異常數據則按照定時任務的執行周期,周期性的達到最終一致性。

回到上面的新版交易系統:游戲中的玩家使用金幣購買道具A,系統將自動贈送給玩家道具B,道具C與道具D。

下圖是使用消息隊列實時觸發數據補償實現最終一致性的時序圖(如看不清楚可以點擊圖片放大):

 上圖中,我們使用TCC事務保證了扣除金幣與添加道具A數據一致,然后發送贈送消息並結束請求,贈送系統收到消息后負責添加道具B、C、D,最終保證數據一致。

 這里如果消息隊列或贈送服務出現異常我們的最終一致性將難以保證,所以我們可以再引入一個定時任務,周期性的觸發異常數據補償。

這樣我們就實現了一個既能保證最終數據一致,又能保證性能的道具買贈系統。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM