【Seata原始碼領讀】揭秘 @GlobalTransactional 背後 RM 的黑盒操作之一階段
來源:架構染色
一、前奏
Seata 從設計層面將事務參與者的角色分為 TC(事務協調器)、RM(資源管理器)、TM(事務管理器) ,傳統的 XA 方案中, RM 是在資料庫層,即依賴了 DB 的 XA 驅動能力,也會有典型的資料鎖定和連線鎖定的問題,為了規避 XA 帶來的制約,Seata 將 RM 從 DB 層遷移出來,以中介軟體的形式放在應用層,完全剝離了分散式事務方案對資料庫在協議支援上的要求。在 Seata 的 AT 模式中, RM 的能力是在資料來源做了一層代理,Seata 在這層代理中干預業務 SQL 執行過程,加入分散式事務所需的邏輯,透過這種方式,Seata 真正做到了對業務程式碼無侵入,只需要透過簡單的配置和宣告,業務方就可享受 Seata 所帶來的分散式事務能力;而且跟 XA 模式相比,當本地事務執行完可以立即釋放本地事務鎖定的資源,效能更好。
二、Seata AT 模式的頂層設計
Seata AT 模式下,一個典型的分散式事務過程如下:
TM 向 TC 申請開啟一個全域性事務,全域性事務建立成功並生成一個全域性唯一的 XID。 XID 在微服務呼叫鏈路的上下文中傳播,供 RM 使用。 RM 向 TC 註冊分支事務,將其納入 XID 對應全域性事務的管轄,執行後上報分支事務狀態。 TM 向 TC 發起針對 XID 的全域性提交或回滾決議。 TC 驅動 RM 完成 XID 下管轄的全部分支事務的提交或回滾操作。
前文《【Seata 原始碼領讀】揭秘 @GlobalTransactional 背後 TM 的黑盒操作》中描述了 TM 的能力,本篇繼續介紹 RM,RM(Resource Manager)叫做資源管理器,控制分支事務,負責 1 階段的分支註冊、狀態彙報,並接收事務協調器 TC 的指令,在 2 階段執行分支(本地)事務的提交和回滾。
在傳統的 XA 方案中 RM 是放在資料庫層的,它依賴了資料庫的 XA 驅動程式,如下圖所示
理論上分散式事務能力下沉,由 DB 提供是好事,但此種 XA 模式有兩個典型的問題:
一是資料鎖定問題,XA 事務過程中,資料是被鎖定的。XA 的資料鎖定是資料庫的內部機制維護的,所以依賴 DBA 干預資料庫去解除資料鎖定。
另一個是連線鎖定問題,XA 事務過程中,連線也是被鎖定的。至少在兩階段提交的 prepare 之前,連線是不能釋放的(因為連線斷開,這個連線上的 XA 分支就會回滾,整個事務也會被迫回滾)。較之於資料的鎖定(資料的鎖定對於事務的隔離性是必要的機制),連線的鎖定帶給整個業務系統的直接影響,限制了併發度。
正式為了規避 XA 方案所帶來的制約,Seata 將 RM 從 DB 層遷移出去,以中介軟體的形式放在應用層,完全剝離了分散式事務方案對資料庫在協議支援上的要求。
Seata AT 模式下 RM 的能力概括來說是在資料來源做了一層代理,當程式執行到 DAO 層,透過 JdbcTemplate 或 Mybatis 操作 DB 時所使用的資料來源實際上用的是 Seata 提供的資料來源代理 DataSourceProxy
,Seata 在這層代理中干預業務 SQL 執行過程,加入分散式事務所需的邏輯,主要是解析 SQL,把業務資料在更新前後的資料映象組織成回滾日誌,並將 undoLog
日誌插入 undo_log
表中,保證每條更新資料的業務 sql 都有對應的回滾日誌存在。透過這種方式,Seata 真正做到了對業務程式碼無侵入,只需要透過簡單的配置,業務方就可以輕鬆享受 Seata 所帶來的功能。
另外這樣做還有效能好處,當本地事務執行完時立即釋放了本地事務鎖定的資源,然後向 TC 上報分支狀態。當 TM 決議全域性事務提交時,就不需要同步呼叫 RM 做什麼處理,而是給 TC 傳送提交指令,委託 TC 以非同步方式排程各個 RM 分支事務刪除對應的 undoLog
日誌即可,這個步驟完成的非常快速;但當 TM 決議全域性回滾時(發生回滾的機率還是很小的),委託 TC 同步向所有 相關 RM 傳送回滾請求,RM 透過 XID 找到對應的 undoLog
回滾日誌,然後構建回滾 sql 並執行,以完成回滾操作。
三、Seata AT 模式 RM 的底層實現
3.1 關鍵類能力簡述
1) DataSourceProxy
構建並向 TC 註冊 Resource 資訊 初始化業務表的後設資料資訊,用於為前後映象的構建和二階段回滾提供基礎能力。
2)ConnectionProxy
提供增強版的 commit,增加的邏輯分兩類:
若上下文中繫結當前全域性事務的 xid,處理分支事務提交
向 TC 註冊分支事務、使用本地事務提交業務 SQL 和 undoLog、向 TC 上報本地 commit 結果;
若上下文中繫結是否需要檢測全域性鎖,處理帶@GlobalLock 的本地事務提交
檢測全域性鎖不存在則提交本地事務
若業務層還顯式的開啟了 JDBC 的事務(AutoCommit 被設定為 false),則提交中還伴有鎖衝突後的重試。
3) StatmentProxy
解析 SQL,根據不同的 SQL 型別委託不同的執行器,構建前後映象生成 undoLog 放置在上下文中。 若業務層未顯式的開啟 JDBC 的事務,則開啟重試機制,並在執行完第一步之後,呼叫 ConnectionProxy 的增強版提交; 若業務層顯式的開啟 JDBC 的事務,則沒有第 2 步中的自動提交
3.2 鳥瞰分支事務的 1 階段處理
Seata AT 模式下,正如下圖的原始碼檢索結果所示,分支事務的執行是在 StatementProxy
、PreparedStatementProxy
的 execute
、executeQuery
、executeUpdate
等方法中,而這些方法最終都會執行到 ExecuteTemplate#execute
方法
所以StatementProxy
和 PreparedStatementProxy
中是委託ExecuteTemplate
完成分支事務的一階段流程
下邊使用虛擬碼,對照官方原理圖,從宏觀視角來描述以下分支事務的一階段邏輯:獲取連結,構建Statement
,之後執行 SQL 解析、根據 SQL 型別構建執行器,由執行器在業務 SQL 執行前後查詢資料快照並組織成 UndoLog
;在提交環節有向 TC 註冊分支事務、UndoLog
的刷盤隨業務 SQL 在本地事務一併 Commit、向 TC 上報分支事務狀態等;若遇到異常會執行本地回滾,上拋異常讓業務邏輯感知;最後釋放資源。
conProxy = mybatis#getConnection()
pareparedStatement = conProxy.PareparedStatement();
pareparedStatementProxy.execute();
ExecuteTemplate.execute
解析SQL構建 xxxExecutor,如 update 對應為 UpdateExecutor
如果autoCommit為true,設定autoCommit為false
LockRetryPolicy.execute//重試策略
AbstractDMLBaseExecutor#executeAutoCommitFalse()
beforImage()//構建前映象
execute()//執行業務sql
afterImage()//構建後映象
connectionProxy.commit // 增強版的提交事務
try
doCommit
register()//向TC註冊分支事務,TC會檢測全域性鎖
flushUndoLogs//undoLog刷盤
try
targetConnection.commit();//使用原始con提交本地事務
catch
report : PhaseOne_Failed //本地commit失敗,向TC上報1階段失敗,丟擲異常
report PhaseOne_Done //向TC 上報 本地commit成功
catch //捕獲到異常就進行回滾
doRollback
targetConnection.rollback();// 執行本地回滾
report : PhaseOne_Failed //跟TC上報本地commit失敗,這裡似乎會重複report
pareparedStatement.close()
con.close()
3.3 詳解分支事務的 1 階段處理
1)基於執行器的設計
如果瞭解過 mybatis 原始碼,會有印象其中關鍵類的命名和執行流程是 xxxTemplate
-呼叫-> yyyExecutor
;Seata 中的實現很相似,是 ExecuteTemplate
-呼叫-> xxxExecutor
。
ExecuteTemplate
分析上下文,構建正確的Executor
Executor
的職責
首先判斷若當前上下文與 Seata 無關(當前即不是 AT 模式的分支事務,又不用檢測全域性鎖),直接使用原始的 Statment
執行業務 SQL,避免因引入 Seata 導致非全域性事務中的 SQL 執行效能下降。解析 SQL 識別 SQL 屬於增刪改查哪種型別,解析結果有快取,因為有些 SQL 解析會比較耗時,可能會導致在應用啟動後剛開始的那段時間裡處理全域性事務中的 SQL 執行效率降低。 對於 INSERT、UPDATE、DELETE、SELECT..FOR UPDATE 等幾類(具體看原始碼)的 sql 使用對應的 Executor
進行處理,其它 SQL (設計初衷這裡是指普通的 select)直接使用原始的Statment
執行。返回執行結果,如有異常則直接拋給上層業務程式碼進行處理。
2)解析 sql,構建 sql 執行器
目前 Seata 1.6.1 版本中 根據 sql 的的型別封裝瞭如INSERT
、UPDATE
、DELETE
、SELECT_FOR_UPDATE
、INSERT_ON_DUPLICATE_UPDATE
、UPDATE_JOIN
這六大類Executor
(執行器)。但從事務處理的能力上有分為 3 大類
PlainExecutor
其中 PlainExecutor
是 原生的 JDBC 介面實現,未做任何處理,提供給全域性事務中的普通的 select 查詢使用
SelectForUpdateExecutor:
Seata 的 AT 模式在本地事務之上預設支援讀未提交的隔離級別,但是透過SelectForUpdateExecutor
執行器,可以支援讀已提交的隔離級別。
前面的文章我們說過用 select...for update
語句來保證隔離級別為讀已提交。SelectForUpdateExecutor
就是用來執行 select...for update
語句的。
先透過 select 檢索記錄,構建出 lockKeys 發給 TC,請 TC 核實這些記錄是否已經被其他事務加鎖了,如果被加鎖了,則根據重試策略不斷重試,如果沒被加鎖,則正常返回查詢的結果。
DML 類的 Executor
DML 增刪改型別的執行器主要在 sql 執行的前後對 sql 語句進行解析,並實現瞭如下兩個抽象介面:
protected abstract TableRecords beforeImage() throws SQLException;
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
這兩個介面便是 AT 模式下 RM 的核心能力:構建 beforeImage
,執行 sql,之後再構建 afterImag
e,透過beforeImage
和 afterImage
生成了提供回滾操作的 undoLog
日誌,不同的執行器這兩個介面的實現不同。
型別 | 構建前映象 | 構建後映象 |
---|---|---|
insert | 否 | 是 |
update | 是 | 是 |
delete | 是 | 否 |
其中構建 update
和 delete
這兩類前映象的 sql 語句的是select ... for update
,其中for update
是一種非常必要的基於本地事務排它機制的隔離保障。
3)執行器的核心方法execute
上下文中設定關鍵的標識資訊:在ConnectionProxy
中設定全域性事務的 XID,則標識後續執行分支事務;如果RootContext.requireGlobalLock()
是true
,則標識後續是處理@GlobalLock
的全域性鎖檢測+本地事務提交。
public T execute(Object... args) throws Throwable {
// 從事務上下文中獲取xid
String xid = RootContext.getXID();
if (xid != null) {
// 將xid繫結到連線的 ConnectionContext 中,由此分支事務感知其所歸屬的全域性事務的xid
statementProxy.getConnectionProxy().bind(xid);
}
// 從上下文中獲取是否需要全域性鎖的標記,傳遞給ConnectionProxy
statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
// 處理sql
return doExecute(args);
}
DML 類執行器的核心邏輯在 AbstractDMLBaseExecutor#doExecute
中,這裡根據是否有開啟 Spring 事務而處理邏輯不通。executeAutoCommitTrue
中會自動提交。而executeAutoCommitFalse
中不執行提交,而由 Spring 事務發起commit
(呼叫的是ConnectionProxy
增強版的commit
)
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
if (connectionProxy.getAutoCommit()) {
// 如果AutoCommit是true,沒有開啟spring事務(即沒有con.setAutoCommit(false)的呼叫)
return executeAutoCommitTrue(args);
} else {
// 如果AutoCommit是false
// 目前已知的情況是由於顯示開啟了事務,保障多條SQL語句的執行只在最後顯式的commit提交後,才生效,
// 如宣告式Spring事務@Transactional,其處理過程會由con.setAutoCommit(false);
// 如果是程式設計式Spring事務,需要顯示呼叫con.setAutoCommit(false);
return executeAutoCommitFalse(args);
}
}
4) executeAutoCommitTrue
執行業務 sql,構建undoLog
並執行增強版的提交。如果有 Spring 事務開啟(AutoCommit
設定為false
),則不執行這個方法,其中有 3 個關鍵邏輯
執行此方法時, Seata 框架將 AutoCommit
設定為false
,在 2.2 中主動commit
目的是 2.1 和 2.2 兩個步驟中的所有本地 sql 同時提交,簡單理解就是 業務 sql 和 Seata 框架的 undoLog 一起提交。
2.1. 業務 sql 的執行(構造前後映象) 2.2. 增強版 commit
(此時,其內部的重試策略無效),下述邏輯根據上下文是三選一直接提交本地事務 申請到全域性鎖後執行本地提交,這種情況下還需要構造前後映象嘛? 執行分支事務的提交,向 TC 申請行鎖,鎖衝突則進入重試邏輯 不衝突執行註冊分支事務,提交本地事務,向 TC 上報結果 2.2.1 processGlobalTransactionCommit()
;2.2.2 processLocalCommitWithGlobalLocks()
;2.2.3 targetConnection.commit()
;
無論第 2 步成功還是失敗,重置上下文,恢復自動提交
第 2 步遇衝突則重試的機制在介紹完 2.1 和 2.2 的主體邏輯後,再補充
protected T executeAutoCommitTrue(Object[] args) throws Throwable {
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
try {
// AutoCommit設定的false,
// 目的是 2.1 和 2.2 兩個步驟中的所有本地sql同時提交,簡單理解就是 業務sql 和 Seata 框架的undoLog一起提交。
connectionProxy.changeAutoCommit();
// 2. 提交過程可能遇到鎖衝突,在遇到鎖衝突時,會有重試策略,重試邏輯中有2個邏輯主體:
return new LockRetryPolicy(connectionProxy).execute(() -> {
// 2.1 業務sql的執行(構造前後映象)
T result = executeAutoCommitFalse(args);
// 2.2 commit(此時,其內部的重試策略無效),下述邏輯根據上下文是三選一
// 2.2.1 processGlobalTransactionCommit();
// 執行分支事務的提交,向TC申請行鎖,鎖衝突則進入重試邏輯
// 不衝突執行註冊分支事務,提交本地事務,向TC上報結果
// 2.2.2 processLocalCommitWithGlobalLocks();
// 申請到全域性鎖後執行本地提交,這種情況下還需要構造前後映象嘛?
// 2.2.3 targetConnection.commit();
// 直接提交本地事務
connectionProxy.commit();
return result;
});
} catch (Exception e) {
// when exception occur in finally,this exception will lost, so just print it here
LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e);
// isLockRetryPolicyBranchRollbackOnConflict() 預設是true,衝突時會重試,則不在這裡回滾
if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) {
connectionProxy.getTargetConnection().rollback();
}
throw e;
} finally {
// 重置上下文
connectionProxy.getContext().reset();
// 設定為自動提交
connectionProxy.setAutoCommit(true);
}
}
5)executeAutoCommitFalse
執行業務 sql,生成前後映象融合成 undoLog,注意此時不提交。
protected T executeAutoCommitFalse(Object[] args) throws Exception {
// 構造beforeImage
TableRecords beforeImage = beforeImage();
// 使用原始Statement 執行sql
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
// 構建afterImage
TableRecords afterImage = afterImage(beforeImage);
// 整合 beforeImage 和 afterImage 構建undoLog
prepareUndoLog(beforeImage, afterImage);
// 返回業務sql的執行結果,並未commit
return result;
}
5.1)整合 beforeImage
和 afterImage
構建 undoLog
根據前後映象構建 lockKeys
和undoLog,暫存到
connectionProxy的上下文中,在下文
commit`方法中才刷盤鎖 key 的構建有其規則,形如 t_user:1_a,2_b
。其中t_user
是表名,第 1 條記錄的主鍵是1
和a
,第 2 條記錄的逐漸是2
和b
;即一條記錄的多個主鍵值之間用*
串聯 ;記錄和記錄之間的 key 資訊用,
串聯;表名和主鍵部分用:
串聯如果是 DELETE 語句,則使用前映象構建 lockKeys
protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
return;
}
if (SQLType.UPDATE == sqlRecognizer.getSQLType()) {
if (beforeImage.getRows().size() != afterImage.getRows().size()) {
throw new ShouldNeverHappenException("Before image size is not equaled to after image size, probably because you updated the primary keys.");
}
}
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
// 如果是DELETE 語句,則使用前映象構建鎖key,
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
// 一條記錄的多個主鍵值之間用_串聯;記錄之間的key資訊用,串聯;
// 形如 t_user:1_a,2_b,第1條記錄的主鍵是1和a,第2條記錄的逐漸是2和b
String lockKeys = buildLockKey(lockKeyRecords);
if (null != lockKeys) {
// lockKeys 暫存到connectionProxy的上下文中,在commit環節,向註冊分支事務環節,這些鎖key被用於檢測全域性行鎖儲存和衝突檢測
connectionProxy.appendLockKey(lockKeys);
// 整合 beforeImage 和 afterImage 構建undoLog
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
// undoLog暫存到connectionProxy的上下文中,在commit環節,才執行undoLog刷盤,伴隨業務SQL在本地事務一起提交
connectionProxy.appendUndoLog(sqlUndoLog);
}
}
6)connectionProxy.commit()
增強版的提交
6.1)connectionProxy.commit()
中的重試
此處的重試很容易讓人迷糊,因為透過對上文的原始碼梳理可知,重試邏輯在資料來源的代理中有兩處。需清楚這兩個重試是互補的,即同一流程中只會有其中一個重試邏輯生效,這兩個重試之間微妙的關係如下:
如果業務 SQL 的執行上下文中,沒有 Spring 的事務,那麼程式會執行
AbstractDMLBaseExecutor.executeAutoCommitTrue
,則其方法中的重試邏輯生效,則此處connectionProxy.commit()
的重試邏輯不啟用如果業務 SQL 的執行上下文中,有 Spring 的事務,那麼程式會執行
AbstractDMLBaseExecutor#executeAutoCommitFalse
,而不會被執行AbstractDMLBaseExecutor.executeAutoCommitTrue
,則此處connectionProxy.commit()
的重試邏輯生效另外
connectionProxy.commit()
與AbstractDMLBaseExecutor.executeAutoCommitTrue
的重試主體對比的話,此處的connectionProxy.commit()
主體只有doCommit
,沒有業務 SQL 的執行以及前後映象的構建,這是重點。為何如此設計筆者理解的尚不夠透徹,望讀者老師能加 V 加群給與解惑。
connectionProxy.commit()
原始碼如下:
public void commit() throws SQLException {
try {
// 這裡的重試 只有 doCommit,沒有業務SQL的執行以及前後映象的構建
// 重試策略在資料來源的代理中從程式碼上看是有兩處,這兩個重試是互補的,也即同一流程中只會有其中一個重試生效。
// 首先如果業務SQL的執行上下文中,沒有Spring的事務,那麼AbstractDMLBaseExecutor.executeAutoCommitTrue 中的重試策略生效,則此處的重試策略不啟用
// 首先如果業務SQL的執行上下文中,有Spring的事務,那麼此處的重試策略生效,而 AbstractDMLBaseExecutor.executeAutoCommitTrue 不會被執行
lockRetryPolicy.execute(() -> {
doCommit();
return null;
});
} catch (SQLException e) {
// 沒有自動提交,也沒有被Seata調整為非自動提交(沒有執行AbstractDMLBaseExecutor.executeAutoCommitTrue)
// 那麼遇到Seata 增強邏輯中丟擲的 SQLException 異常時,在此處執行回滾。並且丟擲異常
// 否則,是由上層發起回滾。
if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
LockRetryController#sleep
方法中控制 重試次數(--lockRetryTimes
) 和 重試間隔(Thread.sleep(lockRetryInterval)
),超過次數上拋異常,退出迴圈。
public void sleep(Exception e) throws LockWaitTimeoutException {
// prioritize the rollback of other transactions
// 重試次數控制
if (--lockRetryTimes < 0 || (e instanceof LockConflictException
&& ((LockConflictException)e).getCode() == TransactionExceptionCode.LockKeyConflictFailFast)) {
throw new LockWaitTimeoutException("Global lock wait timeout", e);
}
try {
// 透過sleep控制重試間隔
Thread.sleep(lockRetryInterval);
} catch (InterruptedException ignore) {
}
}
是否重試是有開關的,在啟動時讀取配置,從 1.6.1 的程式碼來看,未支援執行期變更,預設值是 true
// 在衝突時是否重試的開關
client.rm.lock.retry-policy-branch-rollback-on-conflict=true
重試間隔和次數可在透過配置中心做執行時變更,預設值如下:
// 重試間隔 lockRetryInterval 預設值 10 毫秒
client.rm.lock.retry-interval=10
// 重試次數 lockRetryTimes 預設值 30次
client.rm.lock.retry-times=30
6.2)doCommit()
中的 3 種選擇
增強版的提交程式碼中有下述三種提交邏輯,根據上下文只選其一
processGlobalTransactionCommit();
執行分支事務的提交,向 TC 申請行鎖,鎖衝突則向上反饋後進入上層的重試邏輯 不衝突執行註冊分支事務,提交本地事務,向 TC 上報結果
processLocalCommitWithGlobalLocks();
申請到全域性鎖後執行本地提交,這種情況下還需要構造前後映象嘛?
targetConnection.commit();
直接提交本地事務
ConnectionProxy#doCommit
原始碼如下:
private void doCommit() throws SQLException {
// xid不為空
// 如果 BaseTransactionalExecutor.execute 中 透過 statementProxy.getConnectionProxy().bind(xid) 在context繫結了xid
// 其內部是 context.bind(xid); 那麼此處context.inGlobalTransaction() = true
// 則執行增強版的分支事務提交
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
}
// 如果開發者使用@GlobalLock,則 BaseTransactionalExecutor.execute 中
// 透過statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock())
// 在context繫結了全域性鎖標識,那麼此處context.isGlobalLockRequire() = true
// 則執行增強版的檢測不到全域性鎖才做本地事務提交
else if (context.isGlobalLockRequire()) {
//申請到全域性鎖後執行本地提交
processLocalCommitWithGlobalLocks();
} else {
// 既不是分支事務,又不是@Globallock,那使用原生的本地事務提交
targetConnection.commit();
}
}
6.4)processGlobalTransactionCommit
處理分支事務的提交
前文BaseTransactionalExecutor#execute
中如果識別出有全域性事務的 xid,則給ConnectionProxy
的ConnectionContext
上繫結 xid,表明資料來源代理層是要做分支事務的處理。
所以如果那麼此處context.inGlobalTransaction()
就等於 true,則透過processGlobalTransactionCommit
處理分支事務的提交,在這個方法中是分支事務處理核心中的核心:
註冊分支事務,申請全域性行鎖,如果鎖衝突則丟擲異常,重試機制識別到衝突的異常後做重試處理 undoLog 刷盤 執行本地事務提交,會將本地業務 sql 和 undoLog 一起提交 將本地事務提交的結果(1 階段的處理結果)上報給 TC,TC 若在二階段回滾,而分支事務上報的是 1 階段失敗了,則無需通知此分支事務做 2 階段回滾;否則通知分支事務做 2 階段回滾 重置上下文
private void processGlobalTransactionCommit() throws SQLException {
try {
// 1. 註冊分支事務,申請全域性行鎖,如果鎖衝突則丟擲異常
// 有沒有重複註冊的情況呢?
register();
} catch (TransactionException e) {
// 如果異常code是 LockKeyConflict 和 LockKeyConflictFailFast 才重新組織丟擲異常 LockConflictException
// 外部的重試管控,識別出LockConflictException後實施重試。
// 其他異常此處不處理
recognizeLockKeyConflictException(e, context.buildLockKeys());
}
try {
// 2. 寫undoLog
UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
// 3. 執行本地事務提交,將本地業務sql和undoLog一起提交
targetConnection.commit();
} catch (Throwable ex) {
LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
// 4. 向TC上報異常,並丟擲SQLException,告訴TC二階段若回滾則此分支事務無需回滾,因為1階段失敗了。
report(false);
throw new SQLException(ex);
}
if (IS_REPORT_SUCCESS_ENABLE) {
// 4. 上報事務處理結果,告訴TC二階段若回滾則此分支事務必須回滾,因為1階段成功了。
report(true);
}
// 5. 重試上下文
context.reset();
}
6.5)report(false)
預設只上報 commit 異常
預設是隻上報 commit 失敗的情況,開關是 client.rm.reportSuccessEnable
預設值是 false上報異常會重試,預設 5 次機會 閾值 REPORT_RETRY_COUNT
其配置為client.rm.reportRetryCount
預設值是 5提交失敗一定要上報 BranchStatus.PhaseOne_Failed
告訴 TC 二階段若回滾則此分支事務無需回滾,因為 1 階段失敗了。
// client.rm.reportRetryCount = 5
// 成員變數,構建新物件時就會讀取新到配置中心的新值,也算支援執行期配置變更
private static final int REPORT_RETRY_COUNT = ConfigurationFactory.getInstance().getInt(
ConfigurationKeys.CLIENT_REPORT_RETRY_COUNT, DEFAULT_CLIENT_REPORT_RETRY_COUNT);
// client.rm.reportSuccessEnable = false
public static final boolean IS_REPORT_SUCCESS_ENABLE = ConfigurationFactory.getInstance().getBoolean(
ConfigurationKeys.CLIENT_REPORT_SUCCESS_ENABLE, DEFAULT_CLIENT_REPORT_SUCCESS_ENABLE);
private void report(boolean commitDone) throws SQLException {
if (context.getBranchId() == null) {
return;
}
int retry = REPORT_RETRY_COUNT;//client.rm.reportRetryCount 預設值是 5,不支援執行期變更
while (retry > 0) {
try {
DefaultResourceManager.get().branchReport(BranchType.AT, context.getXid(), context.getBranchId(),
commitDone ? BranchStatus.PhaseOne_Done : BranchStatus.PhaseOne_Failed, null);
return;
} catch (Throwable ex) {
LOGGER.error("Failed to report [" + context.getBranchId() + "/" + context.getXid() + "] commit done ["
+ commitDone + "] Retry Countdown: " + retry);
retry--;
if (retry == 0) {
throw new SQLException("Failed to report branch status " + commitDone, ex);
}
}
}
}
6.6)向 TC 註冊分支事務,並申請全域性行鎖,如果全域性行鎖申請成功才意味著註冊成功,返回分支事務branchId
,儲存在上下文中。
private void register() throws TransactionException {
// 不需要回滾,或不需要全域性鎖,就不註冊
if (!context.hasUndoLog() || !context.hasLockKey()) {
return;
}
// 向TC傳送 BranchRegisterRequest 請求
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), context.getApplicationData(),
context.buildLockKeys());
// 將branchId繫結到上下文中,同一時刻,一個con上只有一個分支事務
context.setBranchId(branchId);
}
6.7)若向 TC 註冊分支事務時,因行鎖衝突導致註冊失敗,則會丟擲鎖衝突的異常LockConflictException
,前邊提到過重試邏輯中會識別此異常後執行重試,這個重試邏輯就在LockRetryPolicy#doRetryOnLockConflict
中。
protected <T> T doRetryOnLockConflict(Callable<T> callable) throws Exception {
LockRetryController lockRetryController = new LockRetryController();
// 迴圈
while (true) {
try {
return callable.call();
} catch (LockConflictException lockConflict) {
// 衝突的情況下,清空context,執行本地rollback();
onException(lockConflict);
// AbstractDMLBaseExecutor#executeAutoCommitTrue the local lock is released
if (connection.getContext().isAutoCommitChanged()
&& lockConflict.getCode() == TransactionExceptionCode.LockKeyConflictFailFast) {
// 這個轉換,目前還未搞清楚用意
lockConflict.setCode(TransactionExceptionCode.LockKeyConflict);
}
// sleep方法裡 重試 和 間隔控制;
// 超過次數丟擲異常,退出迴圈
lockRetryController.sleep(lockConflict);
} catch (Exception e) {
// 其他異常情況下,清空context,執行本地rollback();
onException(e);
// 上拋異常
throw e;
}
}
}
對於分支事務來說,這其中return callable.call();
對應的就是下圖中紅框圈注的內容
但需特別注意LockRetryPolicy#onException
這個方法是空的,但AbstractDMLBaseExecutor.LockRetryPolicy
重寫了onException
方法,在這個方法中會清除上邊重試主體執行過程暫存在上下文中的的鎖 key 和 undoLog,並透過原始Connection
執行回滾。
protected void onException(Exception e) throws Exception {
ConnectionContext context = connection.getContext();
//UndoItems can't use the Set collection class to prevent ABA
//清除構建undoLog時,暫存在上下文中的的鎖key 和 undoLog
context.removeSavepoint(null);
// 透過原始con 執行回滾
connection.getTargetConnection().rollback();
}
從重試的管控邏輯分析可知,AbstractDMLBaseExecutor.LockRetryPolicy
的邏輯是這樣,當遇到鎖衝突異常後,會在onException
中清除上下文,執行回滾之後重試,當重試次數達到上限後,還是會上拋異常。另外若遇到的並非鎖衝突類的異常,會在onException
中清除上下文,執行回滾,之後上跑異常。
異常上拋後,業務上層會接收到該異常,至於是給 TM 模組返回成功還是失敗,由業務上層實現決定,如果返回失敗,則 TM 識別到異常後,會裁決對全域性事務進行回滾。
四、如果跟 GlobalLock 相關
簡單來說 RM 在資料來源代理層的邏輯為
向 TC 查詢鎖是否存在,全域性事務的鎖還存在就透過拋異常繼續重試 如果向 TC 查詢鎖不存在,則提交本地事務。
詳情下篇介紹
參考:
https://mp.weixin.qq.com/s/EzmZ-DAi-hxJhRkFvFhlJQ https://blog.csdn.net/zzti_erlie/article/details/120939588
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2953013/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 揭秘仿比心app原始碼的開發背後,功能是如何實現的APP原始碼
- dyld背後的故事&原始碼分析原始碼
- 槽位背後 | AI專家系統的5個階段(上篇)AI
- 槽位背後 | AI專家系統的5個階段(下篇)AI
- Seata分散式事務TA模式原始碼解讀分散式模式原始碼
- Seata原始碼分析——SessionManager原始碼Session
- JDK 原始碼 Integer解讀之一(parseInt)JDK原始碼
- Guava 原始碼分析(Cache 原理【二階段】)Guava原始碼
- gin原始碼閱讀之一 – net/http的大概流程原始碼HTTP
- 收款神器!解讀聚合收款碼背後的原理
- 袋鼠雲程式碼檢查服務,揭秘高質量程式碼背後的秘密
- 不敢閱讀 npm 包原始碼?帶你揭祕 taro init 背後的哲學NPM原始碼
- React 原始碼解析系列 - React 的 render 階段(三):completeUnitOfWorkReact原始碼
- React 原始碼解析系列 - React 的 render 階段(二):beginWorkReact原始碼
- 從原始碼角度剖析 setContentView() 背後的機制原始碼View
- MapReduce —— MapTask階段原始碼分析(Input環節)APT原始碼
- MapReduce —— MapTask階段原始碼分析(Output環節)APT原始碼
- 分散式事務中介軟體 Fescar—RM 模組原始碼解讀分散式原始碼
- 避免 rm 誤操作
- 開原始碼力榜背後的演算法模型原始碼演算法模型
- [原始碼解析] Flink UDAF 背後做了什麼原始碼
- 技術揭秘:yargs-parser漏洞背後的修復之道
- 滴滴全民拼車日背後的運維技術揭秘運維
- 揭秘Supermicro擴大全球產能的背後競爭力
- 揭秘!KubeSphere 背後的“超級大腦”:etcd 的魅力與力量
- 【原始碼&庫】Vue3 中的 nextTick 魔法背後的原理原始碼Vue
- Flink sql 之 兩階段聚合與 TwoStageOptimizedAggregateRule(原始碼分析)SQLZed原始碼
- Java併發體系-第四階段-AQS原始碼解讀-[1]-【萬字長文系列】JavaAQS原始碼
- 揭秘2019 雙11背後的阿里巴巴超強網路阿里
- 揭秘阿里雲WAF背後神秘的AI智慧防禦體系阿里AI
- 揭秘“資料咖啡”瑞幸背後的大資料危機大資料
- 小白的進階之路之vue原始碼解讀(0)Vue原始碼
- 【原始碼分析】Lottie 實現炫酷動畫背後的原理原始碼動畫
- [Apache Doris] Apache Doris 後設資料設計及DDL操作原始碼閱讀Apache原始碼
- Spark SQL原始碼解析(四)Optimization和Physical Planning階段解析SparkSQL原始碼
- SEO專案操作在不同階段的工作分配
- "淘寶大資料揭秘:購物狂歡節背後的秘密!"大資料
- 揭秘LOL背後的IT基礎架構丨產品而非服務架構