Seata AT模式的整體流程

東平王北星發表於2020-10-29

AT模式的整體流程

at模式的整體流程

先從官網借一張圖,簡單描述AT模式的角色和流程
Seata 目前有四種模式,都是基於 TC(事物協調者),TM(事物管理器),RM(資源管理器) 這三個角色完成的

  1. 事務管理器發起全域性事物
  2. 通過RPC呼叫微服務A
  3. 微服務A開啟事物, 通過解析SQL,通過Druid資料來源的API驅動快照的生成 。首先查詢資料庫獲得當前資料的快照(前置映象), 執行資料操作(更新,刪除,插入), 查詢資料庫獲得操作執行後的快照(後置映象)
  4. 微服務A向TC發起分支事物註冊, 執行回滾日誌的插入, 將當前事物提交
  5. 微服務B 和微服務A一樣,執行2-5的流程
  6. 如果微服務呼叫的過程中沒有報錯, 由事物管理器發起全域性事物提交, 否則發起全域性事物回滾
  7. 如果是成功:TC向微服務A和B傳送分支事物提交請求, 將提交請求通過offer的方式插入到阻塞佇列。有 AsyncWorker 非同步批量處理事務的提交, AsyncWorker 非同步批量刪除回滾日誌
  8. 如果是失敗:TC向微服務A和B傳送分支事物回滾請求。 根據 全域性事物ID(XID)和分支事物ID(BranchId)查詢回滾日誌表(undo_log)獲得回滾日誌。 如果回滾日誌存在:將後置映象與當前資料對比,如果資料一致表示可以回滾(沒有發生髒寫),通過回滾日誌的前置映象生成回滾SQL, 執行資料回滾,然後刪除回滾日誌。 如果回滾日誌不存在:插入一條狀態為全域性事物已完成(資料庫的值是: 1 )的回滾日誌, 避免另一個執行緒提交成功
  9. 如果失敗回滾成功,向TC響應回滾成功, 如果回滾失敗,向TC響應回滾失敗並重試,TC會重試發起分支回滾請求(seata服務端有一個定時器,一秒呼叫一次,會遍歷所有需要回滾的會話發起分支回滾請求)

AT模式的分支事物

一階段提交

AT模式的一階段流程由 資料來源代理+SQL識別器 的方式實現

首先回憶jdbc的執行流程

	//通過資料來源獲取連線
	Connection connection = dataSource.getConnection();
	// 獲得 宣告
	PrepareStatement pst = connection.prepareStatement();
	// 執行SQL語句
	pst.executeUpdate();
	// 提交事務
	connection.commit();

AT模式對 DataSource,Connection,Statement 都做了代理

  1. dataSource 被DataSourceProxy代理, dataSource.getConnection 獲得的物件是 ConnectionProxy 物件, connection.prepareStatement 獲得的是 PreparedStatementProxy 物件
  2. prepareStatement.executeUpdate() 做了特殊了處理, 通過Duird資料來源提供的API建立Seata的SQL識別器,SQL識別器提供了識別SQL語句的功能,用於支援Executor建立前置映象,後置映象。
  3. executor 構建前置映象, 執行業務SQL,構建後置映象, 通過前置映象和後置映象,XID等資料構建回滾日誌物件,新增到ConnectionProxy的上下文
  4. connectionProxy.commit, 註冊分支事物, 根據connectionProxy的上下文物件將回滾日誌生成SQL,執行回滾日誌SQL,真實連線提交,如果配置了一階段提交報告(client.rm.reportSuccessEnable=true,預設是false),則向TC傳送一階段提交完成的請求

prepareStatement.executeUpdate

 public static <T, S extends Statement> T execute(List<SQLRecognizer> sqlRecognizers,
                                                     StatementProxy<S> statementProxy,
                                                     StatementCallback<T, S> statementCallback,
                                                     Object... args) throws SQLException {
        // 不需要全域性鎖或者不是分支模式,執行原始的 statement.execute
        if (!RootContext.requireGlobalLock() && BranchType.AT != RootContext.getBranchType()) {
            // Just work as original statement
            return statementCallback.execute(statementProxy.getTargetStatement(), args);
        }
	
        String dbType = statementProxy.getConnectionProxy().getDbType();
        // 通過SQL訪問者工廠建立SQL識別器, Seata在1.3.0的識別器是面向Druid程式設計
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            sqlRecognizers = SQLVisitorFactory.get(
                    statementProxy.getTargetSQL(),
                    dbType);
        }
        // 執行器
        Executor<T> executor;
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            executor = new PlainExecutor<>(statementProxy, statementCallback);
        } else {
            if (sqlRecognizers.size() == 1) {
                SQLRecognizer sqlRecognizer = sqlRecognizers.get(0);
                switch (sqlRecognizer.getSQLType()) {
                    case INSERT:
                    // 1.3.0 支援Mysql,Oracle,PGSql 的插入執行器
                        executor = EnhancedServiceLoader.load(InsertExecutor.class, dbType,
                                new Class[]{StatementProxy.class, StatementCallback.class, SQLRecognizer.class},
                                new Object[]{statementProxy, statementCallback, sqlRecognizer});
                        break;
                    case UPDATE:
                        executor = new UpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    case DELETE:
                        executor = new DeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    case SELECT_FOR_UPDATE:
                        executor = new SelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    default:
                        executor = new PlainExecutor<>(statementProxy, statementCallback);
                        break;
                }
            } else {
                executor = new MultiExecutor<>(statementProxy, statementCallback, sqlRecognizers);
            }
        }
        T rs;
        try {
       		 // 執行器去執行
            rs = executor.execute(args);
        } catch (Throwable ex) {
            if (!(ex instanceof SQLException)) {
                // Turn other exception into SQLException
                ex = new SQLException(ex);
            }
            throw (SQLException) ex;
        }
        return rs;
    }

executor 的執行

 @Override
    public T execute(Object... args) throws Throwable {
        String xid = RootContext.getXID();
        // 繫結全域性事物ID到代理連線
        if (xid != null) {
            statementProxy.getConnectionProxy().bind(xid);
        }
		// 設定全域性鎖的狀態
        statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
        // 執行
        return doExecute(args);
    }
    
	@Override
    public T doExecute(Object... args) throws Throwable {
        AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
        // 判斷當前連線是否開啟了自動提交, 這裡看executeAutoCommitFalse的部分。 
        // 開啟自動提交的部分關掉自動提交,然後呼叫了下面的部分,然後恢復自動提交為true
        if (connectionProxy.getAutoCommit()) {
            return executeAutoCommitTrue(args);
        } else {
            return executeAutoCommitFalse(args);
        }
    }
    // 執行自動提交
     protected T executeAutoCommitFalse(Object[] args) throws Exception {
        if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && getTableMeta().getPrimaryKeyOnlyName().size() > 1) {
            throw new NotSupportYetException("multi pk only support mysql!");
        }
        // 抽象方法, 子類Mysql,Oracle,PGSql 會知道如何構建前置映象
        TableRecords beforeImage = beforeImage();
        // 執行業務SQL
        T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
        // 通過前置映象構建後置映象
        TableRecords afterImage = afterImage(beforeImage);
        // 通過前置映象和後置映象生成回滾日誌,插入到代理連線的上下文
        prepareUndoLog(beforeImage, afterImage);
        return result;
    }

    protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
    	// 如果前置映象為空,並且後置映象也是空,就不用構建回滾日誌了
        if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
            return;
        }
        ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
        TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
        String lockKeys = buildLockKey(lockKeyRecords);
        // 新增lockKey
        connectionProxy.appendLockKey(lockKeys);
		// 構建回滾日誌
        SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
        // 將回滾日誌新增到代理連線的上下文中
        connectionProxy.appendUndoLog(sqlUndoLog);
    }

代理連線提交

 private void processGlobalTransactionCommit() throws SQLException {
        try {
			// 註冊分支事物
            register();
        } catch (TransactionException e) {
            recognizeLockKeyConflictException(e, context.buildLockKeys());
        }
        try {
        	// 插入回滾日誌
            UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
            // 真實連線提交
            targetConnection.commit();
        } catch (Throwable ex) {
            LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
            report(false);
            throw new SQLException(ex);
        }
        // 是否報告一階段提交完成,預設為false
        if (IS_REPORT_SUCCESS_ENABLE) {
            report(true);
        }
        context.reset();
    }

二階段提交

AT模式的資源管理器(RMHandlerAT) 接受事物協調者(TC)的分支提交請求

  1. 由資源管理器(RMHandlerAT)執行分支提交請求
  2. AT模式的資源管理器內部由非同步工作器(asyncWorker)執行, 將請求用非阻塞(offer)的方式插入到blockingQueue中
  3. asyncWorker內部有一個定時器, 1秒鐘執行一次(在上次執行完之後)。 定時器不停的用非阻塞的(poll)方式從阻塞佇列中獲取資料,然後批量刪除回滾日誌

資料來源管理器的分支事物提交

	// dataSourceManager 的 branchCommit
    @Override
    public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                     String applicationData) throws TransactionException {
         // 由非同步工作器代理,執行分支提交
        return asyncWorker.branchCommit(branchType, xid, branchId, resourceId, applicationData);
    }
    // asyncWorker 的 branchCommit
 	@Override
    public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                     String applicationData) throws TransactionException {
         // 用非阻塞的方式二階段上下文到阻塞佇列
        if (!ASYNC_COMMIT_BUFFER.offer(new Phase2Context(branchType, xid, branchId, resourceId, applicationData))) {
            LOGGER.warn("Async commit buffer is FULL. Rejected branch [{}/{}] will be handled by housekeeping later.", branchId, xid);
        }
        return BranchStatus.PhaseTwo_Committed;
    }

批量刪除回滾日誌

  private void doBranchCommits() {
        if (ASYNC_COMMIT_BUFFER.isEmpty()) {
            return;
        }

        Map<String, List<Phase2Context>> mappedContexts = new HashMap<>(DEFAULT_RESOURCE_SIZE);
        List<Phase2Context> contextsGroupedByResourceId;
        
        while (!ASYNC_COMMIT_BUFFER.isEmpty()) {
        	// 從阻塞佇列批量獲取二階段上下文
            Phase2Context commitContext = ASYNC_COMMIT_BUFFER.poll();
            contextsGroupedByResourceId = CollectionUtils.computeIfAbsent(mappedContexts, commitContext.resourceId, key -> new ArrayList<>());
            contextsGroupedByResourceId.add(commitContext);
        }
		// 省略遍歷 mappedContexts  獲得xids,branchIds 的程式碼,和大量的try,catch 和無關程式碼
        //批量刪除回滾日誌, 構造一個刪除語句:  delete from undu_log where xid in (?) and branch_id in (?)
        UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).batchDeleteUndoLog(
               xids, branchIds, conn);
    }

二階段回滾

二階段回滾由事物協調者(TC)發起, 微服務的資源管理器執行的操作

AT模式由 RMHandlerAT#handle(BranchRollbackRequest request) 處理

  1. 通過全域性事物ID(xid)和分支事物id(branchId)查詢回滾日誌表(undo_log)獲得回滾日誌
  2. 通過資料庫型別和回滾日誌建立執行器(Executor)
  3. 由執行器驅動資料回滾, 首先進行資料驗證,驗證通過則回滾
    • 如果相等就不用執行資料回滾,然後對比前置映象和當前物件,
    • 如果相等就不用執行資料回滾,
    • 如果後置映象和當前物件不相等就丟擲髒資料檢查異常,
    • 如果後置映象和當前物件相等,執行資料回滾。
  4. 如果查詢到了回滾日誌, 刪除回滾日誌。 如果沒查詢到回滾日誌, 插入一條狀態全域性事物已完成的回滾日誌 。

執行器的資料驗證

  protected boolean dataValidationAndGoOn(Connection conn) throws SQLException {

        TableRecords beforeRecords = sqlUndoLog.getBeforeImage();
        TableRecords afterRecords = sqlUndoLog.getAfterImage();

     // 對比前置映象和後置映象, 相同則表示驗證失敗,驗證失敗就不做資料回滾
        Result<Boolean> beforeEqualsAfterResult = DataCompareUtils.isRecordsEquals(beforeRecords, afterRecords);
        if (beforeEqualsAfterResult.getResult()) {
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Stop rollback because there is no data change " +
                        "between the before data snapshot and the after data snapshot.");
            }
            // no need continue undo.
            return false;
        }

        //查詢當前資料
        TableRecords currentRecords = queryCurrentRecords(conn);
      // 對比後置映象和當前資料
        Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
        if (!afterEqualsCurrentResult.getResult()) {

            // 前置映象和當前資料一樣, 驗證失敗,資料不回滾
            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
            if (beforeEqualsCurrentResult.getResult()) {
                return false;
            } else {
             // 發生了髒寫, 丟擲異常
                throw new SQLException("Has dirty records when undo.");
            }
        }
        return true;
    }

刪除或者插入併發日誌

// 如果通過xid和branchId查詢回滾日誌表的資料是存在的
if (exists) {
//刪除回滾日誌
       deleteUndoLog(xid, branchId, conn);
       // 提交事務
       conn.commit();
       if (LOGGER.isInfoEnabled()) {
           LOGGER.info("xid {} branch {}, undo_log deleted with {}", xid, branchId,
               State.GlobalFinished.name());
       }
   } else {
   // 通過全域性事物已完成的回滾日誌, 全域性事物已完成的狀態碼: 1
       insertUndoLogWithGlobalFinished(xid, branchId, UndoLogParserFactory.getInstance(), conn);
       conn.commit();
       if (LOGGER.isInfoEnabled()) {
           LOGGER.info("xid {} branch {}, undo_log added with {}", xid, branchId,
               State.GlobalFinished.name());
       }
   }

相關文章