基於 XA 事務協議,用代碼實現一個二階段分佈式事務

在上篇《漫談分佈式事務的那些解決方案》文章中,我提到了分佈式事務的三種通用解決方案,但是沒有具體的代碼實現,有少小夥伴留言說原理知道了,但是還是不會寫代碼,那麼這篇文章就簡單聊一聊基於 XA 事務協議,用代碼來實現二階段提交。

在具體的 Demo 之前,先來補充一點 XA 事務的知識:DTP 模型與 XA 規範

DTP 模型與 XA 規範是由 X/Open 維護,也就是現在的 open group,官方網址:http://www.opengroup.org/。open group 是一個獨立的組織,主要負責制定各種行業技術標準。由各大知名公司或者廠商進行支持,主要有如下公司:

open group 目前有八家公司,華爲就是其中的一家。在分佈式事務處理(Distributed Transaction Processing,簡稱DTP)方面,X/Open主要提供了以下參考文檔:

  • DTP 參考模型: https://pubs.opengroup.org/onlinepubs/9294999599/toc.pdf
  • DTP XA規範: https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf

DTP 模型

在《Distributed Transaction Processing: Reference Model 》 第3版中,規定了構成 DTP 模型的 5個基本元素:

  • 應用程序(Application Program ,簡稱AP):用於定義事務邊界(即定義事務的開始和結束),並且在事務邊界內對資源進行操作,可以簡單理解爲我們的應用程序。
  • 資源管理器(Resource Manager,簡稱RM):如數據庫、文件系統等,並提供訪問資源的方式。
  • 事務管理器(Transaction Manager ,簡稱TM):負責分配事務唯一標識,監控事務的執行進度,並負責事務的提交、回滾等。
  • 通信資源管理器(Communication Resource Manager,簡稱CRM):控制一個TM域(TM domain)內或者跨TM域的分佈式應用之間的通信。
  • 通信協議(Communication Protocol,簡稱CP):提供CRM提供的分佈式應用節點之間的底層通信服務。

DTP 模型元素更深層次的東西可以參考 opengroup 的文檔,接下來聊一聊 DTP 實例,一個 DTP 實例至少包含 AP、RMs、TM 三部分。如下圖所示:

我們可以看出 AP、RMs、TM 三者之間都是有交互的,大概流程如下:

  • AP 從 RMs 中獲取數據庫資源,個人認爲可以簡單的理解成一條數據庫鏈接,就像我們常用的數據連接一樣。
  • TM 事務資源管理器,負責分配事務唯一標識,監控事務的執行進程,並負責事務的提交、回滾等。AP 會將自己的事務綁定到 TM 中,剩下的事情就交給 TM了。
  • TM 根據收集的結果告訴 RMs(具體的數據庫,例如 MySQL ) 是執行回滾還是提交。

那什麼是 XA 協議呢?XA 規範是定義交互接口,從上面的圖中可以看出,整個 DTP 中,有三個交互接口,XA 規範主要是 TM 和 RMs 之間。下面這張圖好理解一些:

好了,關於 DTP 模型與 XA 規範就聊這麼多,具體的可以查看 opengroup 提供的文檔,下面就用我們熟悉的 MySQL 數據庫來實現一個 XA 事務協議的二階段提交。

MySQL 從5.0.3開始支持XA分佈式事務,且只有InnoDB存儲引擎支持。入下圖:

在 MySQL數據庫官網有一個模塊專門講 XA 事務,具體可以查看:

https://dev.mysql.com/doc/refman/5.7/en/xa.html。

其他的我就不說了,這裏我提一下 XA 事務狀態,一個完整的事務流程如下:

  • 1.使用 XA START 來啓動一個 XA 事務,並把它置於 ACTIVE 狀態。
  • 2.對於一個 ACTIVE 狀態的 XA 事務,我們可以執行構成事務的 SQL 語句,然後發佈一個 XA END 語句。XA END 把事務放入 IDLE狀態。
  • 3.對於一個IDLE 狀態XA事務,可以執行一個 XA PREPARE 語句或一個XA COMMIT…ONE PHASE 語句:
    • XA PREPARE 把事務放入 PREPARED 狀態。在此點上的 XA RECOVER 語句將在其輸出中包括事務的 xid 值,因爲 XA RECOVER 會列出處於 PREPARED 狀態的所有 XA 事務。
    • XA COMMIT…ONE PHASE 用於預備和提交事務。xid 值將不會被 XA RECOVER 列出,因爲事務終止。
  • 對於一個 PREPARED 狀態的 XA事務,您可以發佈一個 XA COMMIT 語句來提交和終止事務,或者發佈XA ROLLBACK來回滾並終止事務。

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

可能你注意到了上面有一個 XID 值,簡單的講一下,MySQL 中使用xid來作爲一個事務分支的標識符。關於 xid 在 XA 規範中有定義,XA規範定義了一個xid有4個部分組成:

  • gtrid:全局事務標識符(global transaction identifier),最大不能超過64字節。
  • bqual:分支限定符(branch qualifier),最大不能超過64字節。
  • data:xid的值,其是 gtrid和bqual拼接後的內容。
  • formatId:formatId的作用就是記錄gtrid、bqual的格式,類似於memcached中flags字段的作用。

好了,關於 XA 事務就 BB 這麼多了,接下來,我們通過一個實例,來實現一把基於 XA 事務協議的二階段提交。

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

假設: 現在有兩個庫:xa_account(賬戶庫,現金庫)、xa_red_account(紅包庫)。兩個庫下面都有一張 account 表,account 表中的字段也比較簡單,就 id、user_id、balance_amount 三個字段,SQL 我就不貼了。

好了,具體代碼如下:

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();
        }
    }
}

運行程序,可以看到如下結果:

從圖中可以清楚看出 XA 事務兩階段提交過程,更多細節請查閱 MySQL 數據庫 XA Transactions 模塊。

今天的分享就這些,希望這篇文章對你的學習或者工作有所幫助,如何你覺得文章不錯的話,可以關注和分享給其他小夥伴,讓更多人學習,感謝。

歡迎關注公衆號【互聯網平頭哥】。關注這個互聯網苟且偷生的程序員,願你我共同進步,今天最好的是明天最低的要求。

互聯網平頭哥

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