由分佈式事務處理模型(XA 規範)引申到Mycat分佈式事務的處理方案

隨着併發量、數據量越來越大及業務已經細化到不能再按照業務劃分,我們不得不使用分佈式數據庫提高系統的性能。在分佈式系統中,各個節點在物理上都是相對獨立的,每個節點上的數據操作都可以滿足 ACID。但是,各獨立節點之間無法知道其他節點事務的執行情況,如果想讓多臺機器中的數據保存一致,就必須保證所有節點上的數據操作要麼全部執行成功,要麼全部不執行,比較常規的解決方法是引入“協調者”來統一調度所有節點的執行。

XA 規範

  X/Open 組織(即現在的 Open Group)定義了分佈式事務處理模型。X/Open DTP 模型(1994)包括應用程序(AP)、事務管理器(TM)、資源管理器(RM)、通信資源管理器(CRM)四部分。事務管理器(TM)是交易中間件,資源管理器(RM)是數據庫,通信資源管理器(CRM)是消息中間件。通常把一個數據庫內部的事務處理看作本地事務,而分佈式事務處理的對象是全局事務。全局事務是指在分佈式事務處理環境中,多個數據庫可能需要共同完成一個工作,這個工作就是一個全局事務。在一個事務中可能更新幾個不同的數據庫,此時一個數據庫對自己內部所做操作的提交不僅需要本身的操作成功,還需要全局事務相關的其他數據庫的操作成功。如果任一數據庫的任一操作失敗,則參與此事務的所有數據庫所做的所有操作都必須回滾。XA就是X/Open DTP 定義的交易中間件與數據庫之間的接口規範(即接口函數),交易中間件用它來通知數據庫事務的開始、結束、提交、回滾等,XA 接口函數由數據庫廠商提供,根據這一思想衍生出二階段提交協議和三階段提交協議。

二階段提交

  所謂的兩個階段是指準備階段和提交階段。 
  準備階段指事務協調者(事務管理器)向每個參與者(資源管理器)發送準備消息,每個參與者要麼直接返回失敗消息(如權限驗證失敗),要麼在本地執行事務,寫本地的 redo 和undo日誌但不提交,可以進一步將準備階段分爲以下三步。 
  (1)協調者節點向所有參與者節點詢問是否可以執行提交操作(vote),並開始等待各參與者節點的響應。 
  (2)參與者節點執行詢問發起爲止的所有事務操作,並將 undo 信息和 redo 信息寫入日誌。 
  (3)各參與者節點響應協調者節點發起的詢問。如果參與者節點的事務操作實際執行成功,則它返回一個“同意”消息;如果參與者節點的事務操作實際執行失敗,則它返回一個“中止”消息。 
  提交階段指如果協調者收到了參與者的失敗消息或者超時,則直接向每個參與者發送回滾(Rollback)消息,否則發送提交(Commit)消息,參與者根據協調者的指令執行提交或者回滾操作,釋放所有事務在處理過程中使用的鎖資源。 
  二階段提交所存在的缺點如下。 
  (1)同步阻塞問題,在執行過程中所有參與節點都是事務阻塞型的,當參與者佔用公共資源時,其他第三方節點訪問公共資源時不得不處於阻塞狀態。 
  (2)單點故障,由於協調者的重要性,一旦協調者發生故障,則參與者會一直阻塞下去。 
  (3)數據不一致,在二階段提交的第 2 個階段中,當協調者向參與者發送 commit 請求之後發生了局部網絡異常或者在發送 commit 請求的過程中協調者發生了故障,則會導致只有一部分參與者接收到了 commit 請求,而在這部分參與者在接收到 commit 請求之後就會執行commit操作,其他部分未接收到 commit 請求的機器則無法執行事務提交,於是整個分佈式系統便出現了數據不一致的現象。 
  由於二階段提交存在諸如同步阻塞、單點問題、數據不一致、宕機等缺陷,所以,研究者們在二階段提交的基礎上做了改進,提出了三階段提交。

三階段提交

  三階段提交(Three-phase commit,3PC),也叫作三階段提交協議(Three-phase commitprotocol),是二階段提交(2PC)的改進版本。三階段提交把二階段提交的準備階段再次一分爲二,這樣三階段提交就有 CanCommit、PreCommit、DoCommit 三個階段。 
  (1)CanCommit 階段:三階段提交的 CanCommit 階段其實和二階段提交的準備階段很像,協調者向參與者發送 commit 請求,參與者如果可以提交就返回 Yes 響應,否則返回 No 響應。 
  (2)PreCommit 階段:協調者根據參與者的反應情況來決定是否可以記錄事務的 PreCommit操作。根據響應情況,有以下兩種可能。

  • 假如協調者從所有參與者那裏獲得的反饋都是 Yes 響應,則執行事務。
  • 假如有任何一個參與者向協調者發送了 No 響應,或者等待超時之後協調者都沒有接到參與者的響應,則執行事務的中斷。

(3)DoCommit階段:該階段進行真正的事務提交,也可以分爲執行提交、中斷事務兩種執行情況。

  執行提交的過程如下。

  • 協調者接收到參與者發送的ACK響應後,將從預提交狀態進入提交狀態,並向所有參與者發送doCommit請求。
  • 事務提交參與者接收到doCommit請求之後,執行正式的事務提交,並在完成事務提交之後釋放所有的事務資源。
  • 事務提交完之後,向協調者發送ACK響應。
  • 協調者接收到所有參與者的ACK響應之後,完成事務。中斷事務的過程如下。
  • 協調者向所有參與者發送abort請求。
  • 參與者接收到 abort 請求之後,利用其在第 2 個階段記錄的 undo 信息來執行事務的回滾操作,並在完成回滾之後釋放所有的事務資源。
  • 參與者完成事務回滾之後,向協調者發送 ACK 消息。
  • 協調者接收到參與者反饋的 ACK 消息之後,執行事務的中斷。

Mycat 中分佈式事務的實現

  Mycat在1.6版本以後已經完全支持 XA 分佈式強事務類型了,先通過一個簡單的示例來了解Mycat中XA的用法。 
  用戶應用側(AP)的使用流程如下: 
  (1)set autocommit=0 
  在應用層需要設置事務不能自動提交; 
  (2)set xa=on 
  在 SQL 中設置 XA 爲開啓狀態; 
  (3)執行 SQL 
   insert into travelrecord(id,name) values(1,’N’),(6000000,’A’),(321,’D’),(13400000,’C’),(59,’E’); 
  (4)commit 或者 rollback 
  對事務進行提交(提交成功或者回滾異常)。 
  完整的流程圖如圖所示。 
 
  Mycat 內部實現側的實現流程如下: 
  (1)set autocommit=0 
  將 MysqlConnection 中的 autocommit 設置爲 false; 
  (2)set xa=on 
  在Mycat中開啓 XA 事務管理器,用 MycatServer.getInstance().genXATXID()生成 XID,用XA START XID 命令進行 XA 事務開始標記,繼續拼裝 SQL 業務(Mycat 會將上面的 insert 數據分片到不同的節點上),拼裝 XA END XID,XA PREPARE XID 最後進行 1pc 提交併記錄日誌到 tm.log 中,如果 1pc 階段有異常,則直接回滾事務 XA ROLLBACK xid。 
  (3)在多節點 MySQL 中全部進行 2pc 提交(XA COMMIT),提交成功後,事務結束;如果有異常,則對事務進行重新提交或者回滾。 
  Mycat 中的 XA 分佈式事務的異常處理流程如下: 
  (1)一階段 commit 異常:如果 1pc 提交任意一個 mysql 節點無法提交或者異常,則全部節點的事務進行回滾,拋出異常給應用側事務回滾。 
  (2)Mycat Crash Recovery 
  Mycat 崩潰以後,根據 tm.log 事務日誌再進行重啓恢復,mycat 啓動後執行事務日誌查找各個節點中已經 prepared 的 XA 事務,進行 commit 或者 rollback。

1. 相關類說明

  通過用戶應用側發送 set xa = on ; SQL 開啓 Mycat 內部 XA 事務管理器的功能,事務管理器將對 MySQL 數據庫進行 XA 方式的事務管理,具體事務管理功能的實現代碼如下:

  • MySQLConnection:數據庫連接。
  • NonBlockingSession:用戶連接 Session。
  • MultiNodeCoordinator:協調者。
  • CommitNodeHandler:分片提交處理。
  • RollbackNodeHandler:分片回滾處理。

2. 代碼解析

  XA 事務啓動的源碼如下:

public class MySQLConnection extends BackendAIOConnection {
    //設置開啓事務
    private void getAutocommitCommand(StringBuilder sb, boolean autoCommit) {
        if (autoCommit) {
            sb.append("SET autocommit=1;");
        } else {
            sb.append("SET autocommit=0;");
        }
    }
    public void execute(RouteResultsetNode rrn, ServerConnection sc,boolean autocommit) throws UnsupportedEncodingException {
        if(!modifiedSQLExecuted && rrn.isModifySQL()) {
            modifiedSQLExecuted = true;
        }
        //獲取當前事務 ID
        String xaTXID = sc.getSession2().getXaTXID();
        synAndDoExecute(xaTXID, rrn, sc.getCharsetIndex(), sc.getTxIsolation(),autocommit);
    }
……
……//省略此處代碼,建議讀者參考 GitHub 倉庫的 MyCAT-Server 項目的 MySQLConnection.java源碼
}

  用戶應用側設置手動提交以後,Mycat 會在當前連接中加入

  SET autocommit=0;

  將該語句加入到 StringBuffer 中,等待提交到數據庫。 
  用戶連接 Session 的源碼如下:

public class NonBlockingSession implements Session {
    ……
……//省略此處代碼,建議讀者參考 GitHub 倉庫的 MyCAT-Server 項目的 NonBlockingSession.java 源碼
}
SET XA = ON ;語句分析
  1. SET XA = ON ;語句分析

  用戶應用側發送該語句到 Mycat 中,由 SQL 語句解析器解析後交由 SetHandle 進行處理c.getSession2().setXATXEnabled (true); 
  調用 NonBlockSession 中的 setXATXEnable d 方法設置 XA 開關啓動,並生成 XID,代碼如下:

public void setXATXEnabled(boolean xaTXEnabled) {
    LOGGER.info("XA Transaction enabled ,con " + this.getSource());
    if (xaTXEnabled && this.xaTXID == null) {
        xaTXID = genXATXID();
    }
}

     另外,NonBlockSession 會接收來自於用戶應用側的 commit, 調用 commit 方法進行處理事務提交的邏輯。 
  在 commit()方法中,首先會 check 節點個數,一個節點和多個節點分爲不同的處理過程,這裏只講下多個節點的處理方法 checkDistriTransaxAndExecute(); 
  該方法會對多個節點的事務進行提交。 
  協調者的源碼如下:

public class MultiNodeCoordinator implements ResponseHandler {

……

……//省略此處代碼,建議讀者參考 GitHub 倉庫 MyCAT-Server 項目的 MultiNodeCoordinator.java 源碼

}

 
  在 NonBlockSession 的 checkDistriTransaxAndExecute()方法中, NonBlockSession 會話類會調用專門進行多節點協同的 MultiNodeCoordinator 類進行具體的處理,在 MultiNodeCoordinator類中,executeBatchNodeCmd 方法加入 XA 1PC 提交的處理,代碼片段如下:

for (RouteResultsetNode rrn : session.getTargetKeys()) {
    ……
    if (mysqlCon.getXaStatus() == TxState.TX_STARTED_STATE){
        //recovery Log
        participantLogEntry[started] = new
        ParticipantLogEntry(xaTxId,conn.getHost(),0,conn.getSchema(),((MySQLConnection) conn).getXaStatus());
        String[] cmds = new String[]{"XA END " + xaTxId,"XA PREPARE " + xaTxId};
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Start execute the batch cmd : "+ cmds[0] + ";" +cmds[1]+","+"current connection:"+conn.getHost()+":"+conn.getPort());
        }
    mysqlCon.execBatchCmd(cmds);
    }
……
}

  在 MultiNodeCoordinator 類的 okResponse 方法中,則進行 2pc 的事務提交

MySQLConnection mysqlCon = (MySQLConnection) conn;
switch (mysqlCon.getXaStatus()){
    case TxState.TX_STARTED_STATE:
    if (mysqlCon.batchCmdFinished()){
        String xaTxId = session.getXaTXID();
        String cmd = "XA COMMIT " + xaTxId;
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Start execute the cmd :"+cmd+",current host:"+mysqlCon.getHost()+":"+mysqlCon.getPort());
        }
        //recovery log
        CoordinatorLogEntry coordinatorLogEntry =inMemoryRepository.get(xaTxId);
        for(int i=0; i<coordinatorLogEntry.participants.length;i++){
            LOGGER.debug("[In MemoryCoordinatorLogEntry]"+coordinatorLogEntry.participants[i]);
            if(coordinatorLogEntry.participants[i].resourceName.equals(conn.getSchema())){
                coordinatorLogEntry.participants[i].txState =TxState.TX_PREPARED_STATE;
            }
        }
        inMemoryRepository.put(session.getXaTXID(),coordinatorLogEntry);
        fileRepository.writeCheckpoint(inMemoryRepository.getAllCoordinatorLogEntries());
        //send commit
        mysqlCon.setXaStatus(TxState.TX_PREPARED_STATE);
        mysqlCon.execCmd(cmd);
    }
    return;
……
}


  分片事務提交處理的源碼如下:

public class CommitNodeHandler implements ResponseHandler {
    //結束 XA
    public void commit(BackendConnection conn) {
        ……
……//省略此處代碼,建議讀者參考 GitHub 倉庫 MyCAT-Server 項目的 CommitNodeHandler.java源碼
    }
    //提交 XA
    @Override
    public void okResponse(byte[] ok, BackendConnection conn) {
        ……
……//省略此處代碼,建議讀者參考 GitHub 倉庫的 MyCAT-Server 項目的 CommitNodeHandler.java 源碼
}


  在 Mycat 中同樣支持單節點 MySQL 數據庫的 XA 事務處理,在 CommitNodeHandler 類中就是對單節點的 XA 二階段處理,處理方式與 MultiNodeCoordinator 類同,通過 commit 方法進行 1pc 的提交,而通過 okResponse 的方法進行 2pc 階段的事務提交。 
  分片事務回滾處理的源碼如下:

public class RollbackNodeHandler extends MultiNodeHandler {
    ……
……//省略此處代碼,建議讀者參考 GitHub 倉庫的 MyCAT-Server 項目的 RollbackNodeHandler.java 源碼
}


  在 RollbackNodeHandler 的 rollback 方法中加入了對 XA 事務的 rollback 處理,用戶應用側發起的 rollback 會在這個方法中進行處理。

for (final RouteResultsetNode node : session.getTargetKeys()) {
    ……
    //support the XA rollback
    MySQLConnection mysqlCon = (MySQLConnection) conn;
    if(session.getXaTXID()!=null) {
        String xaTxId = session.getXaTXID();
        mysqlCon.execCmd("XA END " + xaTxId + ";");
        mysqlCon.execCmd("XA ROLLBACK " + xaTxId + ";");
    }else {
    conn.rollback();
    }
……
}


  同樣,該方法會對所有的 MySQL 數據庫節點發起 xa rollback 指令。 

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