分佈式事務

分佈式事務相關概念

背景

一般來說,數據庫事務正確執行的四個基本要素的縮寫(ACID):

  • 原子性(Autmic):一個原子事務要麼完整執行,要麼乾脆不執行。也就是說,工作單元中的每項任務都必須正確執行,如果有任一任務執行失敗,則整個事務就會被終止並且此前對數據所作的任何修改都將被撤銷。
    如果所有任務都被成功執行,事務就會被提交,那麼對數據所作的修改將會是永久性的
  • 一致性(Consistency):一致性代表了底層數據存儲的完整性。 也就是說:如果事務是併發多個,系統也必須如同串行事務一樣操作。其主要特徵是保護性和不變性(Preserving an Invariant),以轉賬案例爲例,假設有五個賬戶,每個賬戶餘額是100元,那麼五個賬戶總額是500元,如果在這個5個賬戶之間同時發生多個轉賬,無論併發多少個,比如在A與B賬戶之間轉賬5元,在C與D賬戶之間轉賬10元,在B與E之間轉賬15元,五個賬戶總額也應該還是500元,這就是保護性和不變性。
  • 隔離性(Isolation):隔離性是指事務必須在不干擾其他事務的前提下獨立執行,也就是說,在事務執行完畢之前,其所訪問的數據不能受系統其他部分的影響。
  • 持久性(Durability):持久性指明當系統或介質發生故障時,確保已提交事務的更新數據不能丟失,也就意味着一旦事務提交,DBMS保證它對數據庫中數據的改變應該是永久性的,
    耐得住任何系統故障,持久性可以通過數據庫備份和恢復來保證。

跨應用了怎麼處理?

假設:原本訂單模塊和賬戶模塊是放在一起的,現在需要做服務拆分,拆分成訂單服務,賬戶服務。原本收到充值回調後,可以將修改訂單狀態和增加金幣放在一個mysql事務中完成的,但是呢,因爲服務拆分了,就面臨着需要協調2個服務才能完成這個事務

強一致性、弱一致性、最終一致性

從客戶端角度,多進程併發訪問時,更新過的數據在不同進程如何獲取的不同策略,決定了不同的一致性。對於關係型數據庫,要求更新過的數據能被後續的訪問都能看到,這是強一致性。如果能容忍後續的部分或者全部訪問不到,則是弱一致性。如果經過一段時間後要求能訪問到更新後的數據,則是最終一致性

從服務端角度,如何儘快將更新後的數據分佈到整個系統,降低達到最終一致性的時間窗口,是提高系統的可用度和用戶體驗非常重要的方面。對於分佈式數據系統:

  • N — 數據複製的份數
  • W — 更新數據時需要保證寫完成的節點數
  • R — 讀取數據的時候需要讀取的節點數

如果W+R>N,寫的節點和讀的節點重疊,則是強一致性。例如對於典型的一主一備同步複製的關係型數據庫,N=2,W=2,R=1,則不管讀的是主庫還是備庫的數據,都是一致的。

如果W+R<=N,則是弱一致性。例如對於一主一備異步複製的關係型數據庫,N=2,W=1,R=1,則如果讀的是備庫,就可能無法讀取主庫已經更新過的數據,所以是弱一致性。

CAP理論

Eric Brewer 說,這三個指標不可能同時做到。這個結論就叫做 CAP 定理。

Partition tolerance(分區容錯性)

大多數分佈式系統都分佈在多個子網絡。每個子網絡就叫做一個區(partition)。分區容錯的意思是,區間通信可能失敗。比如,一臺服務器放在中國,另一臺服務器放在美國,這就是兩個區,它們之間可能無法通信。

上圖中,G1 和 G2 是兩臺跨區的服務器。G1 向 G2 發送一條消息,G2 可能無法收到。系統設計的時候,必須考慮到這種情況。一般來說,分區容錯無法避免。

Consistency(一致性)

Consistency 中文叫做"一致性"。意思是,寫操作之後的讀操作,必須返回該值。舉例來說,某條記錄是 v0,用戶向 G1 發起一個寫操作,將其改爲 v1。接下來,用戶的讀操作就會得到 v1。這就叫一致性。

爲了讓 G2 也能變爲 v1,就要在 G1 寫操作的時候,讓 G1 向 G2 發送一條消息,要求 G2 也改成 v1。

Availability(可用性)

 Availability 中文叫做"可用性",意思是隻要收到用戶的請求,服務器就必須給出迴應。即用戶可以選擇向 G1 或 G2 發起讀操作。不管是哪臺服務器,只要收到請求,就必須告訴用戶結果。

三者關係

對於一個分佈式系統而言,分區容錯性是一個最基本的要求。因爲 既然是一個分佈式系統,那麼分佈式系統中的組件必然需要被部署到不同的節點,否則也就無所謂分佈式系統了,因此必然出現子網絡。而對於分佈式系統而言,網 絡問題又是一個必定會出現的異常情況,因此分區容錯性也就成爲了一個分佈式系統必然需要面對和解決的問題。因此係統架構師往往需要把精力花在如何根據業務 特點在C(一致性)和A(可用性)之間尋求平衡。

一致性和可用性,爲什麼不可能同時成立?

如果保證 G2 的一致性,那麼 G1 必須在寫操作時,鎖定 G2 的讀操作和寫操作。只有數據同步後,才能重新開放讀寫。鎖定期間,G2 不能讀寫,沒有可用性不。

如果保證 G2 的可用性,那麼勢必不能鎖定 G2,所以一致性不成立。

BASE

BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的縮寫。BASE理論是對CAP中一致性和可用性權衡的結果,其來源於對大規模互聯網系統分佈式實踐的總結, 是基於CAP定理逐步演化而來的。BASE理論的核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性。

BASE理論面向的是大型高可用可擴展的分佈式系統,和傳統的事物ACID特性是相反的,它完全不同於ACID的強一致性模型,而是通過犧牲強一致性來獲得可用性,並允許數據在一段時間內是不一致的,但最終達到一致狀態。但同時,在實際的分佈式場景中,不同業務單元和組件對數據一致性的要求是不同的,因此在具體的分佈式系統架構設計過程中,ACID特性和BASE理論往往又會結合在一起。

冪等性

冪等性,其實是一個數學概念。冪等函數,或冪等方法,是指可以使用相同參數重複執行,並能獲得相同結果的函數。

  1. f(f(x)) = f(x)

在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。也就是說,同一個方法,使用同樣的參數,調用多次產生的業務結果與調用一次產生的業務結果相同。 這一個要求其實也比較好理解,因爲要保證數據的最終一致性,很多解決防範都會有很多重試的操作,如果一個方法不保證冪等,那麼將無法被重試。 冪等操作的實現方式有多種,如在系統中緩存所有的請求與處理結果、檢測到重複操作後,直接返回上一次的處理結果等。

分佈式事務解決方案

基於XA協議的兩階段提交

XA是X/Open CAE Specification (Distributed Transaction Processing)模型中定義的TM(Transaction Manager)與RM(Resource Manager)之間進行通信的接口。

在XA規範中,數據庫充當RM角色,應用需要充當TM的角色,即生成全局的txId,調用XAResource接口,把多個本地事務協調爲全局統一的分佈式事務。

 

二階段提交是XA的標準實現。它將分佈式事務的提交拆分爲2個階段:prepare和commit/rollback。

2PC模型中,在prepare階段需要等待所有參與子事務的反饋,因此可能造成數據庫資源鎖定時間過長,不適合併發高以及子事務生命周長較長的業務場景。兩階段提交這種解決方案屬於犧牲了一部分可用性來換取的一致性。

saga

1987年普林斯頓大學的Hector Garcia-Molina和Kenneth Salem發表了一篇Paper Sagas,講述的是如何處理long lived transaction(長活事務)。Saga是一個長活事務可被分解成可以交錯運行的子事務集合。其中每個子事務都是一個保持數據庫一致性的真實事務。 

saga的提出,最早是爲了解決可能會長時間運行的分佈式事務(long-running process)的問題。所謂long-running的分佈式事務,是指那些企業業務流程,需要跨應用、跨企業來完成某個事務,甚至在事務流程中還需要有手工操作的參與,這類事務的完成時間可能以分計,以小時計,甚至可能以天計。

而saga,則是一種基於補償的消息驅動的用於解決long-running process的一種解決方案。目標是爲了在確保系統高可用的前提下儘量確保數據的一致性。

還是上面的例子,如果用saga來實現,那就是這樣的流程:服務器A的事務先執行,如果執行順利,那麼事務A就先行提交;如果提交成功,那麼就開始執行事務B,如果事務B也執行順利,則事務B也提交,整個事務就算完成。但是如果事務B執行失敗,那事務B本身需要回滾,這時因爲事務A已經提交,所以需要執行一個補償操作,將已經提交的事務A執行的操作作反操作,恢復到未執行前事務A的狀態。這樣的基於消息驅動的實現思路,就是saga。我們可以看出,saga是犧牲了數據的強一致性,僅僅實現了最終一致性,但是提高了系統整體的可用性。

TCC

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

TCC模型是把鎖的粒度完全交給業務處理。它分爲三個階段:

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

下面對TCC模式下,A賬戶往B賬戶匯款100元爲例子,對業務的改造進行詳細的分析:

匯款服務和收款服務分別需要實現,Try-Confirm-Cancel接口,並在業務初始化階段將其注入到TCC事務管理器中。

[匯款服務]
Try:
    檢查A賬戶有效性,即查看A賬戶的狀態是否爲“轉帳中”或者“凍結”;
    檢查A賬戶餘額是否充足;
    從A賬戶中扣減100元,並將狀態置爲“轉賬中”;
    預留扣減資源,將從A往B賬戶轉賬100元這個事件存入消息或者日誌中;
Confirm:
    不做任何操作;
Cancel:
    A賬戶增加100元;
    從日誌或者消息中,釋放扣減資源。
[收款服務]
Try:
    檢查B賬戶賬戶是否有效;
Confirm:
    讀取日誌或者消息,B賬戶增加100元;
    從日誌或者消息中,釋放扣減資源;
Cancel:
    不做任何操作。

缺點:TCC模型對業務的侵入強,改造的難度大。

本地消息表

本地消息表這種實現方式應該是業界使用最多的,其核心思想是將分佈式事務拆分成本地事務進行處理,這種思路是來源於ebay。

基本思路就是:

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

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

生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。如果有靠譜的自動對賬補賬邏輯,這種方案還是非常實用的。

舉例:

第一步,僞代碼如下,對用戶id爲A的賬戶扣款1000元,通過本地事務將事務消息(包括本地事務id、支付賬戶、收款賬戶、金額、狀態等)插入至消息表: 

Begin transaction         

update user_account set amount = amount - 1000 where userId = 'A' 
//更新狀態到本地消息表        
insert into trans_message(xid,payAccount,recAccount,amount,status) 
values(uuid(),'A','B',1000,1);

end transactioncommit;

第二步,通知對方用戶id爲B,增加1000元,通常通過消息MQ的方式發送異步消息,對方訂閱並監聽消息後自動觸發轉賬的操作;這裏爲了保證冪等性,防止觸發重複的轉賬操作,需要在執行轉賬操作方新增一個trans_recv_log表用來做冪等,在第二階段收到消息後,通過判斷trans_recv_log表來檢測相關記錄是否被執行,如果未被執行則會對B賬戶餘額執行加1000元的操作,並會將該記錄增加至trans_recv_log,事件結束後通過回調更新trans_message的狀態值。
 

Begin transaction  
/**讀取消息, B賬戶加1000
.....
*/
update trans_message set status = 0 where xid = ?
end transactioncommit;

事務消息

事務消息作爲一種異步確保型事務, 將兩個事務分支通過MQ進行異步解耦,事務消息的設計流程同樣借鑑了兩階段提交理論,整體交互流程如下圖所示:

  1. 事務發起方首先發送prepare消息到MQ。
  2. 在發送prepare消息成功後執行本地事務。
  3. 根據本地事務執行結果返回commit或者是rollback。
  4. 如果消息是rollback,MQ將刪除該prepare消息不進行下發,如果是commit消息,MQ將會把這個消息發送給consumer端。
  5. 如果執行本地事務過程中,執行端掛掉,或者超時,MQ將會不停的詢問其同組的其它producer來獲取狀態。
  6. Consumer端的消費成功機制有MQ保證。

有一些第三方的MQ是支持事務消息的,比如RocketMQ,但是市面上一些主流的MQ都是不支持事務消息的,比如 RabbitMQ 和 Kafka 都不支持。

幾個要解決的問題:

  • 如果第5步確認發送失敗:

如果確認消息發送失敗了怎麼辦?RocketMQ會定期掃描消息集羣中的事物消息,如果發現了prepare狀態的消息(既不是提交也不是回滾的中間狀態),它會向消息發送者確認本地事務是否已執行成功, 然後再根據我們配置文件配置的處理策略來決定是繼續發送還是回滾

  • 保證消費者不重複消費消息

RocketMQ、Kafka都不保證消息不重複,如果你的業務需要保證嚴格的不重複消息,那麼就需要在我們的業務端保存消費狀態,進行去重。保存消費者消費的狀態即保證每條消息都有唯一編號,在消費者那邊保證消息處理成功後,將狀態寫入到去重表中,每次消費消息都查詢去重表中是否已經存在這個id的消費記錄

  • 解決消費失敗:報警系統+人工處理

如果在消費者那邊出現了邏輯業務上的異常Exception, 在普通情況下可以考慮回滾來解決, 但是在消息中間件這個系統下,系統複雜度將大大提升,且很容易出現Bug,估計出現Bug的概率會比消費失敗的概率大很多。所以針對消費失敗這種情況,最好的辦法就是通過報警系統及時發現失敗情況然後再人工處理。其實爲了交易系統更可靠,我們一般會在類似交易這種高級別的服務代碼中,加入詳細日誌記錄的,一旦系統內部引發類似致命異常要及時通過短信(釘釘、郵件)通知給業務操作人員。

最大努力通知

 整體思路與事務消息類似,與前面異步確保型操作不同的一點是, 在消息由MQ Server投遞到消費者之後, 允許在達到最大重試次數之後正常結束事務.

  1. 業務活動的主動方,在完成業務處理之後,向業務活動的被動方發送消息,允許消息丟失。
  2. 主動方可以設置時間階梯型通知規則,在通知失敗後按規則重複通知,直到通知N次後不再通知。
  3. 主動方提供校對查詢接口給被動方按需校對查詢,用於恢復丟失的業務消息。
  4. 業務活動的被動方如果正常接收了數據,就正常返回響應,並結束事務。
  5. 如果被動方沒有正常接收,根據定時策略,向業務活動主動方查詢,恢復丟失的業務消息

適用場景:

  • 適用於對業務最終一致性的時間敏感度低的系統;

  • 適合跨企業的系統間的操作,或者企業內部比較獨立的系統間的操作,比如銀行通知、商戶通知deng;

方案對比

  • 2PC/3PC需要資源管理器(mysql, redis)支持XA協議,且整個事務的執行期間需要鎖住事務資源,會降低性能。故先排除。
  • TCC的模式,需要事務接口提供try,confirm,cancel三個接口,提高了編程的複雜性。需要依賴於業務方來配合提供這樣的接口。推行難度大。

  • 最大努力通知型,應用於異構或者服務平臺當中

  • ebay的經典模式中,分佈式的事務,是通過本地事務+可靠消息,來達到事務的最終一致性的。但是出現了事務消息,就把本地事務的工作給涵蓋在事務消息當中了。

 

別人的做法

alipay的分佈式事務服務DTS

參考地址:https://tech.antfin.com/docs/2/46887

分佈式事務服務(Distributed Transaction Service,簡稱 DTS)是一個分佈式事務框架,用來保障在大規模分佈式環境下事務的最終一致性。DTS 從架構上分爲 xts-client 和 xts-server 兩部分,前者是一個嵌入客戶端應用的 Jar 包,主要負責事務數據的寫入和處理;後者是一個獨立的系統,主要負責異常事務的恢復。

在 DTS 內部,我們將一個分佈式事務的關聯方,分爲發起方和參與者兩類:

發起方

分佈式事務的發起方負責啓動分佈式事務,觸發創建相應的主事務記錄。發起方是分佈式事務的協調者,負責調用參與者的服務,並記錄相應的事務日誌,感知整個分佈式事務狀態來決定整個事務是 COMMIT 還是 ROLLBACK。

參與者

參與者是分佈式事務中的一個原子單位,所有參與者都必須在一階段接口(Prepare)中標註(Annotation)參與者的標識,它定義了 prepare、 commit、 rollback 3個基本接口,業務系統需要實現這3個接口,並保證其業務數據的冪等性,也必須保證 prepare 中的數據操作能夠被提交(COMMIT)或者回滾(ROLLBACK)。從存儲結構上,DTS 的事務狀態數據可以分爲主事務記錄(Activity)和分支事務記錄(Action)兩類:

主事務記錄 Activity:整個分佈式事務的主體,其最核心的數據結構是事務號(TX_ID)和事務狀態(STATE),它是在啓動分佈式事務的時候持久化寫入數據庫的,它的狀態決定了這筆分佈式事務的狀態。

分支事務記錄 Action:分支事務記錄是主事務記錄的一個子集,它記錄了一個參與者的信息,其中包括參與者的 NAME 名稱,DTS 通過這個 NAME 來唯一定位一個參與者。通過這個分支事務信息,我們就可以對參與者進行提交或者回滾操作。

eBay 本地消息表

參考地址:https://www.infoq.cn/article/solution-of-distributed-system-transaction-consistency

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

舉個經典的跨行轉賬的例子來描述。 第一步,扣款1W,通過本地事務保證了憑證消息插入到消息表中。 第二步,通知對方銀行賬戶上加1W了。那問題來了,如何通知到對方呢?

通常採用兩種方式:

  • 採用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件
  • 採用定時輪詢掃描的方式,去檢查消息表的數據。

各種第三方支付回調

最大努力通知型。如支付寶、微信的支付回調接口方式,不斷回調直至成功,或直至調用次數衰減至失敗狀態。

參考文檔:

https://www.cnblogs.com/zhang-qc/p/8688258.html

https://segmentfault.com/a/1190000004474543

http://www.ruanyifeng.com/blog/2018/07/cap.html

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