分散式事務-2PC與TCC

語言: CN / TW / HK

theme: channing-cyan

小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。

隨著微服務的發展,需要實現分散式事務的場景越來越多。分散式事務在實現上分為基於補償的方案和基於訊息通知方案兩種型別。

基於補償的方案有2PC、TCC模式、Saga模式、Seata AT模式,它們都可以看成是遵守XA協議或是XA協議的變種。本次只聊2PC和TCC,今後有時間再聊其它模式。

分散式事務定義

分散式事務用於在分散式系統中保證不同節點之間的資料一致性。

分散式事務是相對於單機事務/本地事務而言的,在分散式場景下,一個系統由多個子系統構成,每個子系統有獨立的資料來源。在微服務系統架構中,我們把每個子系統看成是一個微服務,每個微服務都可以維護自己的資料儲存,各自保持獨立,最後通過他們之間的互相呼叫組合出更復雜的業務邏輯。

舉一個簡單例子,在電商系統裡,會有庫存服務、訂單服務,還有業務層的購物服務。生成訂單的時候,需要呼叫庫存服務扣減庫存和呼叫訂單服務插入訂單記錄。我們需要同時保證庫存服務和訂單服務的事務性。這就是分散式事務要解決的問題。

圖片

XA協議


在講分散式事務之前,必然需要先了解XA協議。XA是一個協議,是X/Open組織制定的關於分散式事務的一組標準介面,實現這些介面,便意味支援XA協議。

XA協議有兩個重要貢獻,一是定義了兩個角色,二是定義了相關介面(只有定義無實現)。

角色

XA協議定義了分散式事務參與方的兩個角色:

  • 事務協調者(TM=Transaction Manager,對應例子中的購物服務)

  • 資源管理器/事務參與者(RM=Resource Manager,對應例子中的庫存服務和訂單服務)

介面

XA 規範主要定義了事務管理器(Transaction Manager)和區域性資源管理器(Local Resource Manager)之間的介面。

以下的函式使事務管理器可以對資源管理器進行的操作:

1)xa_open,xa_close:建立和關閉與資源管理器的連線。

2)xa_start,xa_end:開始和結束一個本地事務。

3)xa_prepare,xa_commit,xa_rollback:預提交、提交和回滾一個本地事務。

4)xa_recover:回滾一個已進行預提交的事務。

5)ax_開頭的函式使資源管理器可動態地在事務管理器中進行註冊,並可以對XID(TRANSACTION IDS)進行操作。

6)ax_reg,ax_unreg;允許資源管理器在一個TMS(TRANSACTION MANAGER SERVER)中動態註冊或撤消註冊。

兩階段提交(2PC)

二階段提交(2PC)是XA分散式事務協議的一種實現。其實在XA協議定義的函式中,通過xa_prepare,xa_commit已經能發現XA完整提交分準備和提交兩個階段。

2PC多用於資料庫層面,在業務層面使用2PC需要處理很多問題,用的相對少一些。本次主要聊資料庫層面上的2PC。下面我們看一下兩階段提交的流程:

流程

準備階段

準備階段有如下三個步驟:

  • 協調者向所有參與者傳送事務內容,詢問是否可以提交事務,並等待所有參與者答覆。

  • 各參與者執行事務操作,將 undo 和 redo 資訊記入事務日誌中(但不提交事務)。

  • 如參與者執行成功,給協調者反饋 yes,即可以提交;如執行失敗,給協調者反饋 no,即不可提交。

提交階段

協調者基於各個事務參與者的準備狀態,來決策是事務提交Commit()或事務回滾Rollback()。

如果協調者收到了參與者的失敗訊息或者超時,直接給每個參與者傳送回滾(rollback)訊息;否則,傳送提交(commit)訊息。

參與者根據協調者的指令執行提交或者回滾操作,釋放所有事務處理過程中使用的鎖資源。(注意:必須在最後階段釋放鎖資源)

樣例

接下來分兩種情況分別討論提交階段的過程。

提交事務

情況 1,當所有參與者均反饋 yes,提交事務,如下圖所示:

  • 協調者向所有參與者發出正式提交事務的請求(即 commit 請求)。

  • 參與者執行 commit 請求,並釋放整個事務期間佔用的資源。

  • 各參與者向協調者反饋 ack(應答)完成的訊息。

  • 協調者收到所有參與者反饋的 ack 訊息後,即完成事務提交。

圖片

中斷事務

情況 2,當準備階段中任何一個參與者反饋 no,中斷事務,如下圖所示:

  • 協調者向所有參與者發出回滾請求(即 rollback 請求)。

  • 參與者使用階段 1 中的 undo 資訊執行回滾操作,並釋放整個事務期間佔用的資源。

  • 各參與者向協調者反饋 ack 完成的訊息。

  • 協調者收到所有參與者反饋的 ack 訊息後,即完成事務中斷。

圖片

虛擬碼

資料庫

XA 事務,通過 Start 啟動一個 XA 事務,並且被置為 Active 狀態,處在 active 狀態的事務可以執行 SQL 語句,通過 END 方法將 XA 事務置為 IDLE 狀態。處於 IDLE 狀態可以執行 PREPARE 操作或者 COMMIT…ONE PHASE 操作,也就是二階段提交中的第一階段,PREPARED 狀態的 XA事務的時候就可以 Commit 或者 RollBack,也就是二階段提交的第二階段。

「場景:」 模擬現金 + 紅包組合支付,假設我們購買了 100 塊錢的東西,90塊使用現金支付,10 塊紅包支付,現金和紅包處在不同的庫。

「假設:」 現在有兩個庫:xa_account(賬戶庫,現金庫)、xa_red_account(紅包庫)。兩個庫下面都有一張 account 表,account 表中的欄位也比較簡單,就 id、user_id、balance_amount 三個欄位。

```java public class XaDemo { public static void main(String[] args) throws Exception{

    // 是否開啟日誌
    boolean logXaCommands = true;

    // 獲取賬戶庫的 rm(ap做的事情)
    Connection accountConn = DriverManager.getConnection("jdbc:mysql://106.12.12.xxxx:3306/xa_account?useUnicode=true&characterEncoding=utf8","root","xxxxx");
    XAConnection accConn = new MysqlXAConnection((JdbcConnection) accountConn, logXaCommands);
    XAResource accountRm = accConn.getXAResource();
    // 獲取紅包庫的RM
    Connection redConn = DriverManager.getConnection("jdbc:mysql://106.12.12.xxxx:3306/xa_red_account?useUnicode=true&characterEncoding=utf8","root","xxxxxx");
    XAConnection Conn2 = new MysqlXAConnection((JdbcConnection) redConn, logXaCommands);
    XAResource redRm = Conn2.getXAResource();

// XA 事務開始了 // 全域性事務 byte[] globalId = UUID.randomUUID().toString().getBytes(); // 就一個標識 int formatId = 1;

    // 賬戶的分支事務
    byte[] accBqual = UUID.randomUUID().toString().getBytes();;
    Xid xid = new MysqlXid(globalId, accBqual, formatId);

    // 紅包分支事務
    byte[] redBqual = UUID.randomUUID().toString().getBytes();;
    Xid xid1 = new MysqlXid(globalId, redBqual, formatId);
    try {
        // 賬號事務開始 此時狀態:ACTIVE
        accountRm.start(xid, XAResource.TMNOFLAGS);
        // 模擬業務
        String sql = "update account set balance_amount=balance_amount-90 where user_id=1";
        PreparedStatement ps1 = accountConn.prepareStatement(sql);
        ps1.execute();
        accountRm.end(xid, XAResource.TMSUCCESS);
// 賬號 XA 事務 此時狀態:IDLE
        // 紅包分支事務開始
        redRm.start(xid1, XAResource.TMNOFLAGS);
        // 模擬業務
        String sql1 = "update account set balance_amount=balance_amount-10 where user_id=1";
        PreparedStatement ps2 = redConn.prepareStatement(sql1);
        ps2.execute();
        redRm.end(xid1, XAResource.TMSUCCESS);


        // 第一階段:準備提交 
        int rm1_prepare = accountRm.prepare(xid);
        int rm2_prepare = redRm.prepare(xid1);

// XA 事務 此時狀態:PREPARED
// 第二階段:TM 根據第一階段的情況決定是提交還是回滾 boolean onePhase = false; //TM判斷有2個事務分支,所以不能優化為一階段提交 if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) { accountRm.commit(xid, onePhase); redRm.commit(xid1, onePhase); } else { accountRm.rollback(xid); redRm.rollback(xid1); }

    } catch (Exception e) {
        // 出現異常,回滾
        accountRm.rollback(xid);
        redRm.rollback(xid1);
        e.printStackTrace();
    }
}

}

```

業務

一般分為協調器和若干事務執行者兩種角色:

  1. 首先協調器先將Prepare()訊息寫到本機日誌,然後向所有事務執行者發Prepare()訊息 ;

  2. 事務執行者收到Prepare()訊息後,根據本機執行情況,如果成功返回Yes,不成功返回No,返回前把要返回的訊息寫到日誌裡。

  3. 協調器收集完所有事務執行者的返回訊息後(或經過一個超時週期後) ,如果都返回的是Yes,則事務成功,傳送給所有執行者Commit(),否則認為事務失敗傳送Rollback()。

  4. 協調器傳送前還是應把訊息寫到日誌裡。

  5. 執行者接收到協調者的Commit()或Rollback()後先把訊息寫到日誌裡,然後再根據訊息提交或回滾。

注:協調者或事務執行者把傳送或接收到的訊息先寫到日誌裡,主要是為了故障後恢復用。如某一事務執行者從故障中恢復後,先檢查本機的日誌,如果已收到Commit(),則提交,如果已收到Rollback()則回滾。如果是Yes,則再向協調者詢問一下,確定下一步怎麼做。如果日誌裡什麼都沒有,則很可能在Prepare階段事務執行者就崩潰了,因此需要回滾。

二階段提交的缺陷在於如果事務協調者崩潰,所有執行者可能都需要等待協調者,從而產生阻塞。

按照這種實現思路,唯一理論上兩階段提交出現問題的情況是當協調者發出提交指令後宕機並出現磁碟故障等永久性錯誤,導致事務不可追蹤和恢復。

異常情況

上面的流程都是理想狀態,但網路往往沒有這麼理想,會產生很多中間狀態,讓我們看幾種異常情況:

  1. 在準備階段,事務協調者故障,故障時間為傳送Prepare後

  2. 無解:傳送後故障,參與者會一直處於鎖定狀態

  3. 提交階段,參與者超時未返回或網路問題導致部分參與者未收到資訊

  4. 無解:不知道參與者是什麼狀態

  5. 提交階段,事務協調者故障,故障時間分發送Confirm前、傳送過程中、傳送後

  6. 無解:傳送前故障,參與者會一直處於鎖定狀態

  7. 無解:傳送中故障,不知道之前的決策結果

  8. 無解:傳送後故障,不知道完全結束沒有

純2PC方案,對於很多異常情況,無法處理。要解決這些問題,需要增加新的特性,就不算2PC了。

2PC總結

將提交分成兩階段進行的目的很明確,就是儘可能晚地提交事務,讓事務在提交前儘可能地完成所有能完成的工作,這樣,最後的提交階段將是一個耗時極短的微小操作,這種操作在一個分散式系統中失敗的概率是非常小的,也就是所謂的“網路通訊危險期”非常的短暫,這是兩階段提交確保分散式事務原子性的關鍵所在。

2PC 方案實現起來簡單,但太過單薄,所以實際專案中使用比較少,總結一下原因:

  • 效能:在階段一需要所有的參與者都返回狀態後才能進入第二階段,並且要把相關的全域性資源鎖定住,這種同步阻塞的操作,會影響整體事務的併發度。

  • 協議:2PC要求RM必須實現XA協議,準確講XA是一個規範,它只是定義了一系列的介面,只是目前大多數實現XA的都是資料庫或者MQ,在微服務架構中,RM可能是任意的型別,可以是一個微服務,也可以是一個KV

  • 可靠性:如果協調者存在單點故障問題,如果協調者出現故障,參與者將一直處於鎖定狀態。

  • 資料不一致:在二階段提交的階段中,當協調者向參與者傳送commit請求之後,發生了局部網路異常或者在傳送commit請求過程中協調者發生了故障,這會導致只有一部分參與者接受到了commit請求。而在這部分參與者接到commit請求之後就會執行commit操作,但是其他未接到commit請求的機器則無法執行事務提交。於是整個分散式系統便出現了資料不一致性的現象。

TCC

TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 於 2007 年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。

TCC本質上是一個業務層面上的2PC,他要求業務在使用TCC模式時必須實現三個介面Try()、Confirm()和Cancel()。在講述2PC的時候,我們說過2PC無法解決宕機問題,那TCC如何解決2PC無法應對宕機問題的缺陷的呢?答案是不斷重試。

TCC 是服務化的二階段程式設計模型,其 Try、Confirm、Cancel 3 個方法均由業務編碼實現:

  • Try 操作作為一階段,負責資源的檢查和預留。

  • Confirm 操作作為二階段提交操作,執行真正的業務。

  • Cancel 是預留資源的取消。

流程

我們以上面的電商下單為例進行分析。

Try階段

Try 僅是一個初步操作,它和後續的Confirm一起才能真正構成一個完整的業務邏輯,這個階段主要完成:

  • 完成所有業務檢查( 一致性 ) 。

  • 預留必須業務資源( 準隔離性 ) 。

  • Try 嘗試執行業務。

假設商品庫存為 100,購買數量為 2,這裡檢查和更新庫存的同時,凍結使用者購買數量的庫存,同時建立訂單,訂單狀態為待確認。

圖片

Confirm/Cancel階段

根據 Try 階段服務是否全部正常執行,繼續執行確認操作(Confirm)或取消操作(Cancel)。

Confirm 和 Cancel 操作滿足冪等性,如果 Confirm 或 Cancel 操作執行失敗,將會不斷重試直到執行完成。

Confirm:當 Try 階段服務全部正常執行, 執行確認業務邏輯操作

圖片

這裡使用的資源一定是 Try 階段預留的業務資源。在 TCC 事務機制中認為,如果在 Try 階段能正常的預留資源,那 Confirm 一定能完整正確的提交。

Confirm 階段也可以看成是對 Try 階段的一個補充,Try+Confirm 一起組成了一個完整的業務邏輯。

Cancel:當 Try 階段存在服務執行失敗, 進入 Cancel 階段

圖片

Cancel 取消執行,釋放 Try 階段預留的業務資源,上面的例子中,Cancel 操作會把凍結的庫存釋放,並更新訂單狀態為取消。

設計要點

空回滾

如果協調者的Try()請求因為網路超時失敗,那麼協調者在階段二時會發送Cancel()請求,而這時這個事務參與者實際上之前並沒有執行Try()操作而直接收到了Cancel()請求。

針對這個問題,TCC模式要求在這種情況下Cancel()能直接返回成功,也就是要允許「空回滾」。

防懸掛

接著上面的問題1,Try()請求超時,事務參與者收到Cancel()請求而執行了空回滾,但就在這之後網路恢復正常,事務參與者又收到了這個Try()請求,所以Try()和Cancel()發生了懸掛,也就是先執行了Cancel()後又執行了Try()

針對這個問題,TCC模式要求在這種情況下,事務參與者要記錄下Cancel()的事務ID,當發現Try()的事務ID已經被回滾,則直接忽略掉該請求。

冪等性

Confirm()和Cancel()的實現必須是冪等的。當這兩個操作執行失敗時協調者都會發起重試。

虛擬碼

  1. 初始化:向事務管理器註冊新事務,生成全域性事務唯一ID

  2. try階段執行:try相關的程式碼執行,期間註冊相應的呼叫記錄,傳送try執行結果到事務管理器,執行成功由事務管理器執行confirm或者cancel步驟

  3. confirm階段:事務管理器收到try執行成功資訊,根據事務ID,進入事務confirm階段執行,confirm失敗進入cancel,成功則結束

  4. cancel階段:事務管理器收到try執行失敗或者confirm執行失敗,根據事務ID,進入cancel階段執行後結束,如果失敗了,列印日誌或者告警,讓人工參與處理,也可記錄失敗,系統不斷對cancel進行重試

TCC總結

TCC和2PC看起來很像,TCC和2PC最大的區別是,2PC是偏資料庫層面的,而TCC是純業務層面。

TCC 事務機制相對於傳統事務機制(X/Open XA),TCC 事務機制相比於上面介紹的 XA 事務機制,有以下優點:

  • 效能提升:具體業務來實現控制資源鎖的粒度變小,不會鎖定整個資源。

  • 資料最終一致性:基於 Confirm 和 Cancel 的冪等性,保證事務最終完成確認或者取消,保證資料的一致性。

  • 可靠性:解決了 XA 協議的協調者單點故障問題,由主業務方發起並控制整個業務活動,業務活動管理器也變成多點,引入叢集。

  • 支援度:該模式對有無本地事務控制都可以支援使用面廣。

缺點:TCC 的 Try、Confirm 和 Cancel 操作功能要按具體業務來實現,業務耦合度較高,提高了開發成本。

總結

在看分散式事務的時候,看了一下公司下單程式碼,發現根本沒有使用2PC或者TCC,方案也比較莽:執行正向操作,如果有失敗,則呼叫逆向操作,逆向失敗重試幾次,還失敗就記錄,由另一個系統進行重試。

之所以這麼設計,主要是這套系統設計的比較早,當時還沒這麼複雜場景。一旦系統複雜了,對核心功能的修改難度又較大。雖然系統目前能正常運作,但最終還是需要優化。

如果重新設計的話,我倒是挺喜歡TCC,因為各個系統按規範設計,都實現上述的設計要點,加入中間狀態和重試系統,後面修改、擴充都要容易很多。後面看看有沒有時間,實現一版TCC方案,否則容易一看就懂一寫就廢。不過得先把Saga模式、Seata AT模式和基於訊息通知的方案聊完。

資料

  1. 分散式事務

  2. http://mp.weixin.qq.com/s/0eKX26pAbSYEH1ZtY6sBww

  3. http://github.com/TIGERB

  4. 關於分散式事務,XA協議的學習筆記(整理轉載)

  5. xa

  6. 分散式事務之兩階段提交

  7. 對分散式事務及兩階段提交、三階段提交的理解

  8. 如何理解兩階段提交?

  9. 兩階段提交的工程實踐

  10. 分散式事務和兩階段提交及三階段提交

  11. TCC和兩階段分散式事務處理的區別

  12. 關於分散式事務:兩階段提交,TCC和tx-lcn框架

  13. 分散式事務解決方案-seata實現2pc分散式事務控制

  14. 攀博課堂

  15. 分散式強一致性有哪些實現方案,2PC是不是強一致?

  16. MySQL兩階段提交具體實現

  17. 基於兩階段提交的分散式事務實現(UP-2PC)

  18. Java分散式事務 兩階段提交的編碼實現-TCC

  19. Go分散式事務調研

  20. RocketMQ事務分享

  21. TCC Demo 程式碼實現

最後

大家如果喜歡我的文章,可以關注我的公眾號(程式設計師麻辣燙)

我的個人部落格為:http://shidawuhen.github.io/

往期文章回顧:

  1. 設計模式

  2. 招聘

  3. 思考

  4. 儲存

  5. 算法系列

  6. 讀書筆記

  7. 小工具

  8. 架構

  9. 網路

  10. Go語言