Seata AT模式TransactionHook被刪除探究
前言
兄弟們,剛剛又給seata社區(qū)修了一個BUG,有用戶提了issue反應TransactionHook在某些情況下不會被調用:

相關issue鏈接:github.com/seata/seata…,該用戶在issue中已經(jīng)指出了相關問題所在:

下面我們來看一下到底是什么原因導致了上述BUG的產生。
問題定位
根據(jù)用戶的反饋,我們找到目標源碼io.seata.tm.api.TransactionalTemplate#execute():
try {
// 開啟分布式事務,獲取XID
beginTransaction(txInfo, tx);
Object rs;
try {
// 執(zhí)行業(yè)務代碼
rs = business.execute();
} catch (Throwable ex) {
// 3. 處理異常,準備回滾.
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
// 4. 提交事務.
commitTransaction(tx, txInfo);
return rs;
} finally {
//5. 回收現(xiàn)場
resumeGlobalLockConfig(previousConfig);
triggerAfterCompletion();
cleanUp();
}
問題代碼就出在cleanUp()中,我們來看一下里面做了什么操作,最終我們定位到:
public final class TransactionHookManager {
private static final ThreadLocal<List<TransactionHook>> LOCAL_HOOKS = new ThreadLocal<>();
// 注冊TransactionHook
public static void registerHook(TransactionHook transactionHook) {
if (transactionHook == null) {
throw new NullPointerException("transactionHook must not be null");
}
List<TransactionHook> transactionHooks = LOCAL_HOOKS.get();
if (transactionHooks == null) {
LOCAL_HOOKS.set(new ArrayList<>());
}
LOCAL_HOOKS.get().add(transactionHook);
}
// 移除當前線程上所有TransactionHook
public static void clear() {
LOCAL_HOOKS.remove();
}
}
由上面的源碼可知,cleanUp()操作時把當前線程中的所有TransactionHook都清除掉了。也就是說,假如事務A和事務B共用同一個線程,當事務B處理完畢后,調用了cleanUp()回收現(xiàn)場時,把該線程當中存儲的所有TransactionHook全部清除掉了,導致事務A的生命周期中找不到該事務對應的TransactionHook,從而產生了BUG。
如何解決
通過與seata社區(qū)的大佬不斷地溝通,最終敲定以下方案:
1.改造TransactionHookManager.LOCAL_HOOKS,把數(shù)據(jù)類型改成ThreadLocal<Map<String, List<TransactionHook>>>,Map中的key對應分布式事務XID;
2.針對當前上下文中沒有XID,那么key就為null,因為HashMap允許key為null;
3.當用戶查詢指定XID下的hook時,連同key為null對應的hook也一起返回;
- 第一步比較好理解,因為事務A和事務B對應的
TransactionHook沒有被區(qū)分出來,所以造成了清理事務B的TransactionHook時連同事務A的TransactionHook一起被清除,那么我們修改數(shù)據(jù)結構來區(qū)分事務A和事務B的TransactionHook,以便清理的時候不會造成誤刪;
第二步為什么要針對沒有XID的時候也要能設置TransactionHook,因為有這么一段代碼:
private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException {
try {
// 執(zhí)行triggerBeforeBegin()
triggerBeforeBegin();
// 注冊分布式事務,生成XID
tx.begin(txInfo.getTimeOut(), txInfo.getName());
// 執(zhí)行triggerAfterBegin()
triggerAfterBegin();
} catch (TransactionException txe) {
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.BeginFailure);
}
}
上面的代碼會產生一個問題,因為我們的TransactionHook依賴于XID,但是triggerBeforeBegin()執(zhí)行的時候還沒有產生XID,所以為了能夠在沒有XID的時候也能夠讓TransactionHook生效,我們要有一個虛值key來臨時設置TransactionHook;
第三步的設計時為了在第二步的基礎上,當事務開啟后獲取XID后,要保證XID獲取前注冊的TransactionHook也要生效,我們在通過XID查詢TransactionHook時要把虛值key對應的TransactionHook也一起返回;
注意事項
在實際代碼修改中,發(fā)現(xiàn)triggerAfterCommit()、triggerAfterRollback()、triggerAfterCompletion()在被調用時始終拿不到對應的TransactionHook,最終debug下來發(fā)現(xiàn)在調用這三個方法前,上下文中的XID被解綁了,導致拿到的XID為空。代碼類似下面這樣:
try {
// 調用triggerBeforeCommit()
triggerBeforeCommit();
// 提交事務,清除XID
tx.commit();
if (Arrays.asList(GlobalStatus.TimeoutRollbacking, GlobalStatus.TimeoutRollbacked).contains(tx.getLocalStatus())) {
throw new TransactionalExecutor.ExecutionException(tx,
new TimeoutException(String.format("Global transaction[%s] is timeout and will be rollback[TC].", tx.getXid())),
TransactionalExecutor.Code.TimeoutRollback);
}
// 調用triggerAfterCommit()
triggerAfterCommit();
} catch (TransactionException txe) {
// 4.1 Failed to commit
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.CommitFailure);
}
不過經(jīng)過我的一番查找,發(fā)現(xiàn)GlobalTransaction中是包含XID屬性的,所以果斷從GlobalTransaction對象中取XID傳進來。
修改后的代碼如下:
try {
// 調用triggerBeforeCommit()
triggerBeforeCommit();
// 提交事務,清除XID
tx.commit();
if (Arrays.asList(GlobalStatus.TimeoutRollbacking, GlobalStatus.TimeoutRollbacked).contains(tx.getLocalStatus())) {
throw new TransactionalExecutor.ExecutionException(tx,
new TimeoutException(String.format("Global transaction[%s] is timeout and will be rollback[TC].", tx.getXid())),
TransactionalExecutor.Code.TimeoutRollback);
}
// 調用triggerAfterCommit()
triggerAfterCommit(tx.getXid());
} catch (TransactionException txe) {
// 4.1 Failed to commit
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.CommitFailure);
}
改造后的TransactionHookManager
public final class TransactionHookManager {
private TransactionHookManager() {
}
private static final ThreadLocal<Map<String, List<TransactionHook>>> LOCAL_HOOKS = new ThreadLocal<>();
/**
* get the current hooks
*
* @return TransactionHook list
*/
public static List<TransactionHook> getHooks() {
String xid = RootContext.getXID();
return getHooks(xid);
}
/**
* get hooks by xid
*
* @param xid
* @return TransactionHook list
*/
public static List<TransactionHook> getHooks(String xid) {
Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
if (hooksMap == null || hooksMap.isEmpty()) {
return Collections.emptyList();
}
List<TransactionHook> hooks = new ArrayList<>();
List<TransactionHook> localHooks = hooksMap.get(xid);
if (StringUtils.isNotBlank(xid)) {
List<TransactionHook> virtualHooks = hooksMap.get(null);
if (virtualHooks != null && !virtualHooks.isEmpty()) {
hooks.addAll(virtualHooks);
}
}
if (localHooks != null && !localHooks.isEmpty()) {
hooks.addAll(localHooks);
}
if (hooks.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableList(hooks);
}
/**
* add new hook
*
* @param transactionHook transactionHook
*/
public static void registerHook(TransactionHook transactionHook) {
if (transactionHook == null) {
throw new NullPointerException("transactionHook must not be null");
}
Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
if (hooksMap == null) {
hooksMap = new HashMap<>();
LOCAL_HOOKS.set(hooksMap);
}
String xid = RootContext.getXID();
List<TransactionHook> hooks = hooksMap.get(xid);
if (hooks == null) {
hooks = new ArrayList<>();
hooksMap.put(xid, hooks);
}
hooks.add(transactionHook);
}
/**
* clear hooks by xid
*
* @param xid
*/
public static void clear(String xid) {
Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
if (hooksMap == null || hooksMap.isEmpty()) {
return;
}
hooksMap.remove(xid);
if (StringUtils.isNotBlank(xid)) {
hooksMap.remove(null);
}
}
}以上就是Seata AT模式TransactionHook被刪除探究的詳細內容,更多關于Seata AT刪除TransactionHook的資料請關注腳本之家其它相關文章!
相關文章
Springboot多環(huán)境開發(fā)及使用方法
這篇文章主要介紹了Springboot多環(huán)境開發(fā)及多環(huán)境設置使用、多環(huán)境分組管理的相關知識,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
Eclipse中創(chuàng)建Web項目最新方法(2023年)
在Java開發(fā)人員中,最常用的開發(fā)工具應該就是Eclipse,下面這篇文章主要給大家介紹了關于Eclipse中創(chuàng)建Web項目2023年最新的方法,需要的朋友可以參考下2023-09-09

