Seata AT模式TransactionHook被刪除探究
前言
兄弟們,剛剛又給seata社區(qū)修了一個(gè)BUG
,有用戶提了issue反應(yīng)TransactionHook在某些情況下不會(huì)被調(diào)用:
相關(guān)issue鏈接:github.com/seata/seata…,該用戶在issue中已經(jīng)指出了相關(guān)問(wèn)題所在:
下面我們來(lái)看一下到底是什么原因?qū)е铝松鲜?code>BUG的產(chǎn)生。
問(wèn)題定位
根據(jù)用戶的反饋,我們找到目標(biāo)源碼io.seata.tm.api.TransactionalTemplate#execute()
:
try { // 開(kāi)啟分布式事務(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)場(chǎng) resumeGlobalLockConfig(previousConfig); triggerAfterCompletion(); cleanUp(); }
問(wèn)題代碼就出在cleanUp()
中,我們來(lái)看一下里面做了什么操作,最終我們定位到:
public final class TransactionHookManager { private static final ThreadLocal<List<TransactionHook>> LOCAL_HOOKS = new ThreadLocal<>(); // 注冊(cè)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()
操作時(shí)把當(dāng)前線程中的所有TransactionHook
都清除掉了。也就是說(shuō),假如事務(wù)A和事務(wù)B共用同一個(gè)線程,當(dāng)事務(wù)B處理完畢后,調(diào)用了cleanUp()
回收現(xiàn)場(chǎng)時(shí),把該線程當(dāng)中存儲(chǔ)的所有TransactionHook
全部清除掉了,導(dǎo)致事務(wù)A的生命周期中找不到該事務(wù)對(duì)應(yīng)的TransactionHook
,從而產(chǎn)生了BUG
。
如何解決
通過(guò)與seata社區(qū)的大佬不斷地溝通,最終敲定以下方案:
1.改造TransactionHookManager.LOCAL_HOOKS
,把數(shù)據(jù)類(lèi)型改成ThreadLocal<Map<String, List<TransactionHook>>>
,Map
中的key
對(duì)應(yīng)分布式事務(wù)XID
;
2.針對(duì)當(dāng)前上下文中沒(méi)有XID,那么key
就為null
,因?yàn)?code>HashMap允許key
為null
;
3.當(dāng)用戶查詢指定XID
下的hook
時(shí),連同key
為null
對(duì)應(yīng)的hook
也一起返回;
- 第一步比較好理解,因?yàn)槭聞?wù)A和事務(wù)B對(duì)應(yīng)的
TransactionHook
沒(méi)有被區(qū)分出來(lái),所以造成了清理事務(wù)B的TransactionHook
時(shí)連同事務(wù)A的TransactionHook
一起被清除,那么我們修改數(shù)據(jù)結(jié)構(gòu)來(lái)區(qū)分事務(wù)A和事務(wù)B的TransactionHook
,以便清理的時(shí)候不會(huì)造成誤刪;
第二步為什么要針對(duì)沒(méi)有XID的時(shí)候也要能設(shè)置TransactionHook
,因?yàn)橛羞@么一段代碼:
private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException { try { // 執(zhí)行triggerBeforeBegin() triggerBeforeBegin(); // 注冊(cè)分布式事務(wù),生成XID tx.begin(txInfo.getTimeOut(), txInfo.getName()); // 執(zhí)行triggerAfterBegin() triggerAfterBegin(); } catch (TransactionException txe) { throw new TransactionalExecutor.ExecutionException(tx, txe, TransactionalExecutor.Code.BeginFailure); } }
上面的代碼會(huì)產(chǎn)生一個(gè)問(wèn)題,因?yàn)槲覀兊?code>TransactionHook依賴(lài)于XID
,但是triggerBeforeBegin()
執(zhí)行的時(shí)候還沒(méi)有產(chǎn)生XID
,所以為了能夠在沒(méi)有XID
的時(shí)候也能夠讓TransactionHook
生效,我們要有一個(gè)虛值key
來(lái)臨時(shí)設(shè)置TransactionHook
;
第三步的設(shè)計(jì)時(shí)為了在第二步的基礎(chǔ)上,當(dāng)事務(wù)開(kāi)啟后獲取XID
后,要保證XID
獲取前注冊(cè)的TransactionHook
也要生效,我們?cè)谕ㄟ^(guò)XID
查詢TransactionHook
時(shí)要把虛值key
對(duì)應(yīng)的TransactionHook
也一起返回;
注意事項(xiàng)
在實(shí)際代碼修改中,發(fā)現(xiàn)triggerAfterCommit()
、triggerAfterRollback()
、triggerAfterCompletion()
在被調(diào)用時(shí)始終拿不到對(duì)應(yīng)的TransactionHook
,最終debug下來(lái)發(fā)現(xiàn)在調(diào)用這三個(gè)方法前,上下文中的XID
被解綁了,導(dǎo)致拿到的XID
為空。代碼類(lèi)似下面這樣:
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); }
不過(guò)經(jīng)過(guò)我的一番查找,發(fā)現(xiàn)GlobalTransaction
中是包含XID
屬性的,所以果斷從GlobalTransaction
對(duì)象中取XID
傳進(jìn)來(lái)。
修改后的代碼如下:
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的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
將Java項(xiàng)目打包成可執(zhí)行的jar包
這篇文章主要介紹了將Java項(xiàng)目打包成可執(zhí)行的jar包,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Java正則表達(dá)式易錯(cuò)知識(shí)點(diǎn)匯總
這篇文章主要總結(jié)Java正則表達(dá)式易錯(cuò)知識(shí),對(duì)易錯(cuò)知識(shí)點(diǎn)進(jìn)行分類(lèi)整理,幫助大家更好的學(xué)習(xí)Java正則表達(dá)式,感興趣的小伙伴們可以參考一下2015-12-12java 類(lèi)加載機(jī)制和反射詳解及實(shí)例代碼
這篇文章主要介紹了java 類(lèi)加載機(jī)制和反射詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03Springboot多環(huán)境開(kāi)發(fā)及使用方法
這篇文章主要介紹了Springboot多環(huán)境開(kāi)發(fā)及多環(huán)境設(shè)置使用、多環(huán)境分組管理的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03Eclipse中創(chuàng)建Web項(xiàng)目最新方法(2023年)
在Java開(kāi)發(fā)人員中,最常用的開(kāi)發(fā)工具應(yīng)該就是Eclipse,下面這篇文章主要給大家介紹了關(guān)于Eclipse中創(chuàng)建Web項(xiàng)目2023年最新的方法,需要的朋友可以參考下2023-09-09Java項(xiàng)目打包發(fā)布到maven私倉(cāng)常見(jiàn)的幾種方式
這篇文章主要介紹了項(xiàng)目打包發(fā)布到maven私倉(cāng)常見(jiàn)的幾種方式,幫助大家更好的理解和學(xué)習(xí)使用Java,感興趣的朋友可以了解下2021-03-03