Seata 之 rm-datasource 源碼解讀

本文首發於個人微信公衆號《andyqian》, 期待你的關注!

前言

這篇文章主要講述的是分佈式事務 seata 框架的 rm - datasource 模塊。文章會按照以下幾點進行講解:

  1. 簡介
  2. 結構
  3. 源碼解析
  4. 涉及的設計模式
  5. 後續的擴展

簡介

我們知道seata框架本身是事務的協調者,協調多個本地事務“符合事務的特性”,從而構成一個”分佈式的全局事務”。這也是seata框架的設計思想。看到這裏,我們應該知道,要想將多個本地事務能夠組成一個全局”事務”,就應該對本地事務進行改造,讓其在同一個“全局事務”內。rm - datasource 模塊就是一個這樣的角色,其承擔的職責包括但不限於以下幾點:

  1. SQL 解析與執行。
  2. 全局”事務”的註冊,提交與回滾。
  3. undo 日誌的生成與使用。

結構

rm-datasource 類圖如下所示:

 

 

別看有這麼多類以及package。別慌,這裏有很多我們的老朋友,我們一起來認識一下:

代理類:

StatementProxy,

PreparedStatementProxy,

DataSoureProxy,

ConnectionPrxoy。

這些類分別是我們熟悉的:Statement, PreparedStatement, DataSoure, Connection 的代理類。(下面會以 ConnectionPrxoy 爲例,查看其源碼。)

exec 模塊

該模塊承擔的職責是:對SQL按照不同的SQL類型進行執行,並保存beforeImage以及afterImage鏡像文件到 undo 數據結構中。(用於事務回滾)。

其主要類圖如下所示:

 

  1. 頂層父類是 Executor 接口,其定義了 executor 的方法。代碼也非常簡單,如下所示:
public interface Executor<T> {

 /**
  * Execute t.
  *
  * @param args the args
  * @return the t
  * @throws Throwable the throwable
  */
 T execute(Object... args) throws Throwable;
}
  1. BaseTransactionalExecutor 爲抽象類,實現了Executor 接口中的 execute 方法。並定義了doExecute() 抽象方法。

execute 方法實現如下:

@Override
    public Object execute(Object... args) throws Throwable {
        //1. 如果爲Global 事務,則將XId 綁定在上下文中
        if (RootContext.inGlobalTransaction()) {
            String xid = RootContext.getXID();
            statementProxy.getConnectionProxy().bind(xid);
        }
        //2. 是否需要獲取全局鎖,進行不同設置
        if (RootContext.requireGlobalLock()) {
            statementProxy.getConnectionProxy().setGlobalLockRequire(true);
        } else {
            statementProxy.getConnectionProxy().setGlobalLockRequire(false);
        }
        return doExecute(args);
    }

    /**
     * 定義抽象方法,由子類自行實現。
     * Do execute object.
     * @param args the args
     * @return the object
     * @throws Throwable the throwable
     */
    protected abstract Object doExecute(Object... args) throws Throwable;

在 BaseTransactionalExecutor 抽象類中。還封裝了部分公共方法,例如:
getTableMeta() (獲取表的元信息),buildLockKey (構造全局鎖的key), prepareUndoLog (執行UndoLog信息) 等等。

  1. PlainExecutor 表示: 未使用全局事務時的執行器。(即直接走原生的)
  2. AbstractDMLBaseExecutor 類繼承自 BaseTransactionalExecutor 抽象。實現了父類的 doExecute() 方法,並定義了多個抽象方法。代碼如下所示:
@Override
    public T doExecute(Object... args) throws Throwable {
        AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
        // 是否爲自動提交,
        if (connectionProxy.getAutoCommit()) {
            return executeAutoCommitTrue(args);
        } else {
            return executeAutoCommitFalse(args);
        }
    }

    // 業務SQL 執行前的鏡像 (用於事務回滾)
    protected abstract TableRecords beforeImage() throws SQLException;


    // 業務SQL 執行後的鏡像。(用於事務回滾)
    protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;

sql 模塊

該模塊承擔的職責是:識別SQL,按照不同的SQL類型定義了一系列的識別器。另外該模塊的底層引用到了alibaba.druid 開源庫。類圖如下所示:

 

undo 模塊

該模塊承擔的職責是:undo 對象的管理,其中包括:undo對象的編解碼,undo對象的基本操作。另外在該模塊中還包含:SQL 關鍵字檢查,(個人覺得這部分放在sql模塊裏面更合理些)。

類圖如下所示:

 

源碼解析

以 ConnectionPrxoy 類爲例,commit 方法代碼如下所示:

@Override
 public void commit() throws SQLException {
     // 如果是Global事務,則執行 processGlobalTransactionCommit方法
     if (context.inGlobalTransaction()) {
         processGlobalTransactionCommit();
     // 是否需要獲取全局鎖
     } else if (context.isGlobalLockRequire()) {
         processLocalCommitWithGlobalLocks();
     } else {
       //本地事務直接 commit
         targetConnection.commit();
     }
 }
  1. processGlobalTransactionCommit 方法代碼如下所示:
private void processGlobalTransactionCommit() throws SQLException {
        try {
        //1. 向 server 端進行分支事務註冊
            register();
        } catch (TransactionException e) {
            recognizeLockKeyConflictException(e);
        }

        //2. 如果有 UndoLog 日誌,則將其新增到 undo_log 表中。
        try {
            if (context.hasUndoLog()) {
                UndoLogManager.flushUndoLogs(this);
            }
        //3. 執行本地事務
            targetConnection.commit();
        } catch (Throwable ex) {
            //4. 如果異常,則向 server 端 報告未提交完成。
            report(false);
            if (ex instanceof SQLException) {
                throw new SQLException(ex);
            }
        }
        //5. 向 server 端,報告提交完成。並將ccontext.reset()掉。
        report(true);
        context.reset();
    }
  1. processLocalCommitWithGlobalLocks 方法 代碼如下所示:
private void processLocalCommitWithGlobalLocks() throws SQLException {

        //1. 檢查全局鎖是否存在,如果存在,則拋出異常。
        checkLock(context.buildLockKeys());
        try {
        //
            targetConnection.commit();
        } catch (Throwable ex) {
            throw new SQLException(ex);
        }
        context.reset();
    }

rollback 方法,代碼如下所示:

@Overrid
 public void rollback() throws SQLException {
     //1. 本地事務進行回滾
     targetConnection.rollback();
     //2. 如果是全局事務,且進行過分支註冊,則向server報告未提交完成。
     if (context.inGlobalTransaction()) {
         if (context.isBranchRegistered()) {
             report(false);
         }
     }
     context.reset();
 }

report 方法代碼如下所示:

private void report(boolean commitDone) throws SQLException {
     //1. 獲取重試次數,默認爲5次。可通過 report.retry.count 進行設置。
     int retry = REPORT_RETRY_COUNT;

     while (retry > 0) {
         try {
         //2.  分子報告
             DefaultResourceManager.get().branchReport(BranchType.AT, context.getXid(), context.getBranchId(),
                 (commitDone ? BranchStatus.PhaseOne_Done : BranchStatus.PhaseOne_Failed), null);
             return;
         } catch (Throwable ex) {
            //3. 異常時,則進行重試
             LOGGER.error("Failed to report [" + context.getBranchId() + "/" + context.getXid() + "] commit done ["
                 + commitDone + "] Retry Countdown: " + retry);
             retry--;

             // 4. 達到重試次數,以及失敗,則拋出異常。
             if (retry == 0) {
                 throw new SQLException("Failed to report branch status " + commitDone, ex);
             }
         }
     }
 }

由於篇幅原因,其他的代理類就不詳細講解。有興趣的可以自己看看源碼。

涉及的設計模式

在該模塊中,涉及的設計模式有:

1. 代理模式

典型類有:

DataSourceProxy(數據源代理),

ConnectionProxy (連接代理),

StatementProxy (statement代理)。

2. 工廠模式

典型類有:

SQLVisitorFactory,

KeywordCheckerFactory

UndoExecutorFactory ,

UndoLogParserFactory。

3. 單例模式

典型類有:MySQLKeywordChecker。

部分代碼如下:

public static KeywordChecker getInstance() {
        if (keywordChecker == null) {
            synchronized (MySQLKeywordChecker.class) {
                if (keywordChecker == null) {
                    keywordChecker = new MySQLKeywordChecker();
                    keywordSet = Arrays.stream(MySQLKeyword.values()).map(MySQLKeyword::name).collect(Collectors.toSet());
                }
            }
        }
        return keywordChecker;
    }

4. 模板模式

典型類有:

GlobalLockTemplate,

ExecuteTemplate 。

GlobalLockTemplate 類代碼如下所示:

/**
     * Execute object.
     *
     * @param business the business
     * @return the object
     * @throws Exception
     */
    public Object execute(Callable<T> business) throws Exception {

        Object rs = null;
        try {
            // add global lock declare
            RootContext.bindGlobalLockFlag();

            // Do Your Business
            rs = business.call();
        } finally {
            //clean the global lock declare
            RootContext.unbindGlobalLockFlag();
        }

        return rs;
    }

後續的擴展

通過上面的介紹,我們已經非常清楚這個模塊的職責。根據社區以及框架本身的發展上來說,在後續的迭代過程中包括但不限於以下功能:

  1. 不同數據庫的支持。
    目前僅支持MySQL,Oracle 正在支持中。
  2. 數據庫關鍵字檢查的完善。
  3. datasource 自動代理。


由於篇幅原因,還有很多點沒有涉及到。只能放在後面的文章中進行講解了。如果你也對分佈式事務感興趣,可以留言一起探討。


 

相關閱讀:

Dubbo 線程池源碼解析

你所不知道的 BigDecimal

ThreadPoolExecutor 原理解析

Java線程池ThreadPoolExecutor

 

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