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-03Eclipse中創(chuàng)建Web項目最新方法(2023年)
在Java開發(fā)人員中,最常用的開發(fā)工具應(yīng)該就是Eclipse,下面這篇文章主要給大家介紹了關(guān)于Eclipse中創(chuàng)建Web項目2023年最新的方法,需要的朋友可以參考下2023-09-09