Seata AT模式的整體流程
AT模式的整體流程
先從官網借一張圖,簡單描述AT模式的角色和流程
Seata 目前有四種模式,都是基於 TC(事物協調者),TM(事物管理器),RM(資源管理器) 這三個角色完成的
- 事務管理器發起全域性事物
- 通過RPC呼叫微服務A
- 微服務A開啟事物, 通過解析SQL,通過Druid資料來源的API驅動快照的生成 。首先查詢資料庫獲得當前資料的快照(前置映象), 執行資料操作(更新,刪除,插入), 查詢資料庫獲得操作執行後的快照(後置映象)
- 微服務A向TC發起分支事物註冊, 執行回滾日誌的插入, 將當前事物提交
- 微服務B 和微服務A一樣,執行2-5的流程
- 如果微服務呼叫的過程中沒有報錯, 由事物管理器發起全域性事物提交, 否則發起全域性事物回滾
- 如果是成功:TC向微服務A和B傳送分支事物提交請求, 將提交請求通過offer的方式插入到阻塞佇列。有 AsyncWorker 非同步批量處理事務的提交, AsyncWorker 非同步批量刪除回滾日誌
- 如果是失敗:TC向微服務A和B傳送分支事物回滾請求。 根據 全域性事物ID(XID)和分支事物ID(BranchId)查詢回滾日誌表(undo_log)獲得回滾日誌。 如果回滾日誌存在:將後置映象與當前資料對比,如果資料一致表示可以回滾(沒有發生髒寫),通過回滾日誌的前置映象生成回滾SQL, 執行資料回滾,然後刪除回滾日誌。 如果回滾日誌不存在:插入一條狀態為全域性事物已完成(資料庫的值是: 1 )的回滾日誌, 避免另一個執行緒提交成功
- 如果失敗回滾成功,向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 都做了代理
- dataSource 被DataSourceProxy代理, dataSource.getConnection 獲得的物件是 ConnectionProxy 物件, connection.prepareStatement 獲得的是 PreparedStatementProxy 物件
- prepareStatement.executeUpdate() 做了特殊了處理, 通過Duird資料來源提供的API建立Seata的SQL識別器,SQL識別器提供了識別SQL語句的功能,用於支援Executor建立前置映象,後置映象。
- executor 構建前置映象, 執行業務SQL,構建後置映象, 通過前置映象和後置映象,XID等資料構建回滾日誌物件,新增到ConnectionProxy的上下文
- 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)的分支提交請求
- 由資源管理器(RMHandlerAT)執行分支提交請求
- AT模式的資源管理器內部由非同步工作器(asyncWorker)執行, 將請求用非阻塞(offer)的方式插入到blockingQueue中
- 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) 處理
- 通過全域性事物ID(xid)和分支事物id(branchId)查詢回滾日誌表(undo_log)獲得回滾日誌
- 通過資料庫型別和回滾日誌建立執行器(Executor)
- 由執行器驅動資料回滾, 首先進行資料驗證,驗證通過則回滾
- 如果相等就不用執行資料回滾,然後對比前置映象和當前物件,
- 如果相等就不用執行資料回滾,
- 如果後置映象和當前物件不相等就丟擲髒資料檢查異常,
- 如果後置映象和當前物件相等,執行資料回滾。
- 如果查詢到了回滾日誌, 刪除回滾日誌。 如果沒查詢到回滾日誌, 插入一條狀態全域性事物已完成的回滾日誌 。
執行器的資料驗證
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());
}
}
相關文章
- Seata的AT模式的執行流程模式
- PCBA加工的整體流程
- 深入淺出Seata的AT模式模式
- Seata AT模式學習模式
- App Store上架的整體流程APP
- 微信MMKV原始碼分析(一) | 整體流程原始碼
- vue-router原始碼分析-整體流程Vue原始碼
- 《深入理解Spark》之Spark的整體執行流程Spark
- seata分散式事務AT模式介紹(二)分散式模式
- spring cloud gateway 原始碼解析(1)整體流程SpringCloudGateway原始碼
- 2.2 spring5原始碼 -- ioc載入的整體流程Spring原始碼
- 聊聊Seata分散式解決方案AT模式的實現原理分散式模式
- Seata原始碼分析(一). AT模式底層實現原始碼模式
- Seata-go 1.1.0 釋出,補齊 AT 模式支援Go模式
- 架構設計 | 基於Seata中介軟體,微服務模式下事務管理架構微服務模式
- OkHttp 原始碼剖析系列(二)——攔截器整體流程分析HTTP原始碼
- Seata分散式事務TA模式原始碼解讀分散式模式原始碼
- Seata 分散式事務框架 TCC 模式原始碼分析分散式框架模式原始碼
- 分散式事務與Seate框架(3)——Seata的AT模式實現原理分散式框架模式
- 微服務痛點-基於Dubbo + Seata的分散式事務(AT)模式微服務分散式模式
- 分散式事務中介軟體Seata的設計原理分散式
- 分散式事務 SEATA-1.4.1 AT模式 配合NACOS 應用分散式模式
- 微服務痛點-基於Dubbo + Seata的分散式事務(TCC模式)微服務分散式模式
- 【iOS】IAP內購整個流程iOS
- 安卓ro.serialno產生的整個流程安卓
- (二) Dorker 專案部署和安裝 dockerUI 整體步驟和流程DockerUI
- Scrapy原始碼閱讀分析_1_整體框架和流程介紹原始碼框架
- 通過對抽象模型和概念模型的整合,細化專案整體流程抽象模型
- 最新幹貨get,手機相機專案的整體測試流程是怎樣的?
- 【深入淺出Seata原理及實戰】「入門基礎專題」探索Seata服務的AT模式下的分散式開發實戰指南(2)模式分散式
- B2C電商系統整體功能和流程設計總結
- 分散式事務 Seata TCC 模式深度解析 | SOFAChannel#4 直播整理分散式模式
- seata分散式事務TCC模式介紹及推薦實踐分散式模式
- Spring Cloud Seata系列:基於AT模式實現分散式事務SpringCloud模式分散式
- 研發模式和流程的再思考模式
- 淺析MyBatis(一):由一個快速案例剖析MyBatis的整體架構與執行流程MyBatis架構
- PostgreSQL 原始碼解讀(122)- MVCC#7(提交事務-整體流程)SQL原始碼MVCC#
- Seata-AT模式:MySQL自增ID的場景下推薦在 Mybatis 中使用 useGeneratedKeys模式MySqlMyBatis