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

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

下面我們來看一下到底是什么原因?qū)е铝松鲜?code>BUG的產(chǎn)生。
問題定位
根據(jù)用戶的反饋,我們找到目標(biāo)源碼io.seata.tm.api.TransactionalTemplate#execute():
try {
// 開啟分布式事務(wù),獲取XID
beginTransaction(txInfo, tx);
Object rs;
try {
// 執(zhí)行業(yè)務(wù)代碼
rs = business.execute();
} catch (Throwable ex) {
// 3. 處理異常,準(zhǔn)備回滾.
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
// 4. 提交事務(wù).
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);
}
// 移除當(dāng)前線程上所有TransactionHook
public static void clear() {
LOCAL_HOOKS.remove();
}
}
由上面的源碼可知,cleanUp()操作時把當(dāng)前線程中的所有TransactionHook都清除掉了。也就是說,假如事務(wù)A和事務(wù)B共用同一個線程,當(dāng)事務(wù)B處理完畢后,調(diào)用了cleanUp()回收現(xiàn)場時,把該線程當(dāng)中存儲的所有TransactionHook全部清除掉了,導(dǎo)致事務(wù)A的生命周期中找不到該事務(wù)對應(yīng)的TransactionHook,從而產(chǎn)生了BUG。
如何解決
通過與seata社區(qū)的大佬不斷地溝通,最終敲定以下方案:
1.改造TransactionHookManager.LOCAL_HOOKS,把數(shù)據(jù)類型改成ThreadLocal<Map<String, List<TransactionHook>>>,Map中的key對應(yīng)分布式事務(wù)XID;
2.針對當(dāng)前上下文中沒有XID,那么key就為null,因為HashMap允許key為null;
3.當(dāng)用戶查詢指定XID下的hook時,連同key為null對應(yīng)的hook也一起返回;
- 第一步比較好理解,因為事務(wù)A和事務(wù)B對應(yīng)的
TransactionHook沒有被區(qū)分出來,所以造成了清理事務(wù)B的TransactionHook時連同事務(wù)A的TransactionHook一起被清除,那么我們修改數(shù)據(jù)結(jié)構(gòu)來區(qū)分事務(wù)A和事務(wù)B的TransactionHook,以便清理的時候不會造成誤刪;
第二步為什么要針對沒有XID的時候也要能設(shè)置TransactionHook,因為有這么一段代碼:
private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException {
try {
// 執(zhí)行triggerBeforeBegin()
triggerBeforeBegin();
// 注冊分布式事務(wù),生成XID
tx.begin(txInfo.getTimeOut(), txInfo.getName());
// 執(zhí)行triggerAfterBegin()
triggerAfterBegin();
} catch (TransactionException txe) {
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.BeginFailure);
}
}
上面的代碼會產(chǎn)生一個問題,因為我們的TransactionHook依賴于XID,但是triggerBeforeBegin()執(zhí)行的時候還沒有產(chǎn)生XID,所以為了能夠在沒有XID的時候也能夠讓TransactionHook生效,我們要有一個虛值key來臨時設(shè)置TransactionHook;
第三步的設(shè)計時為了在第二步的基礎(chǔ)上,當(dāng)事務(wù)開啟后獲取XID后,要保證XID獲取前注冊的TransactionHook也要生效,我們在通過XID查詢TransactionHook時要把虛值key對應(yīng)的TransactionHook也一起返回;
注意事項
在實際代碼修改中,發(fā)現(xiàn)triggerAfterCommit()、triggerAfterRollback()、triggerAfterCompletion()在被調(diào)用時始終拿不到對應(yīng)的TransactionHook,最終debug下來發(fā)現(xiàn)在調(diào)用這三個方法前,上下文中的XID被解綁了,導(dǎo)致拿到的XID為空。代碼類似下面這樣:
try {
// 調(diào)用triggerBeforeCommit()
triggerBeforeCommit();
// 提交事務(wù),清除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);
}
// 調(diào)用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傳進(jìn)來。
修改后的代碼如下:
try {
// 調(diào)用triggerBeforeCommit()
triggerBeforeCommit();
// 提交事務(wù),清除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);
}
// 調(diào)用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被刪除探究的詳細(xì)內(nèi)容,更多關(guān)于Seata AT刪除TransactionHook的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot多環(huán)境開發(fā)及使用方法
這篇文章主要介紹了Springboot多環(huán)境開發(fā)及多環(huán)境設(shè)置使用、多環(huán)境分組管理的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
Eclipse中創(chuàng)建Web項目最新方法(2023年)
在Java開發(fā)人員中,最常用的開發(fā)工具應(yīng)該就是Eclipse,下面這篇文章主要給大家介紹了關(guān)于Eclipse中創(chuàng)建Web項目2023年最新的方法,需要的朋友可以參考下2023-09-09

