Seata AT模式TransactionHook会被莫名删除?
创始人
2024-01-21 12:59:30

前言

兄弟们,刚刚又给seata社区修了一个BUG,有用户提了issue反应TransactionHook在某些情况下不会被调用:

该用户在issue中已经指出了相关问题所在:

下面我们来看一下到底是什么原因导致了上述BUG的产生。

问题定位

根据用户的反馈,我们找到目标源码io.seata.tm.api.TransactionalTemplate#execute()

try {// 开启分布式事务,获取XID         beginTransaction(txInfo, tx);
​Object rs;try {// 执行业务代码rs = business.execute();} catch (Throwable ex) {// 3\. 处理异常,准备回滚.completeTransactionAfterThrowing(txInfo, tx, ex);throw ex;}// 4\. 提交事务.commitTransaction(tx, txInfo);
​return rs;
} finally {//5\. 回收现场resumeGlobalLockConfig(previousConfig);triggerAfterCompletion();cleanUp();
}
复制代码

问题代码就出在cleanUp()中,我们来看一下里面做了什么操作,最终我们定位到:

public final class TransactionHookManager {private static final ThreadLocal> LOCAL_HOOKS = new ThreadLocal<>();// 注册TransactionHookpublic static void registerHook(TransactionHook transactionHook) {if (transactionHook == null) {throw new NullPointerException("transactionHook must not be null");}List transactionHooks = LOCAL_HOOKS.get();if (transactionHooks == null) {LOCAL_HOOKS.set(new ArrayList<>());}LOCAL_HOOKS.get().add(transactionHook);}// 移除当前线程上所有TransactionHookpublic static void clear() {LOCAL_HOOKS.remove();}
}
复制代码

由上面的源码可知,cleanUp()操作时把当前线程中的所有TransactionHook都清除掉了。也就是说,假如事务A和事务B共用同一个线程,当事务B处理完毕后,调用了cleanUp()回收现场时,把该线程当中存储的所有TransactionHook全部清除掉了,导致事务A的生命周期中找不到该事务对应的TransactionHook,从而产生了BUG

如何解决

通过与seata社区的大佬不断地沟通,最终敲定以下方案:

1.改造TransactionHookManager.LOCAL_HOOKS,把数据类型改成ThreadLocal>>Map中的key对应分布式事务XID

2.针对当前上下文中没有XID,那么key就为null,因为HashMap允许keynull

3.当用户查询指定XID下的hook时,连同keynull对应的hook也一起返回;

  • 第一步比较好理解,因为事务A和事务B对应的TransactionHook没有被区分出来,所以造成了清理事务B的TransactionHook时连同事务A的TransactionHook一起被清除,那么我们修改数据结构来区分事务A和事务B的TransactionHook,以便清理的时候不会造成误删;

  • 第二步为什么要针对没有XID的时候也要能设置TransactionHook,因为有这么一段代码:

        private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException {try {// 执行triggerBeforeBegin()triggerBeforeBegin();// 注册分布式事务,生成XIDtx.begin(txInfo.getTimeOut(), txInfo.getName());// 执行triggerAfterBegin()triggerAfterBegin();} catch (TransactionException txe) {throw new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.BeginFailure);}}
    复制代码
    

    上面的代码会产生一个问题,因为我们的TransactionHook依赖于XID,但是triggerBeforeBegin()执行的时候还没有产生XID,所以为了能够在没有XID的时候也能够让TransactionHook生效,我们要有一个虚值key来临时设置TransactionHook

  • 第三步的设计时为了在第二步的基础上,当事务开启后获取XID后,要保证XID获取前注册的TransactionHook也要生效,我们在通过XID查询TransactionHook时要把虚值key对应的TransactionHook也一起返回;

注意事项

在实际代码修改中,发现triggerAfterCommit()triggerAfterRollback()triggerAfterCompletion()在被调用时始终拿不到对应的TransactionHook,最终debug下来发现在调用这三个方法前,上下文中的XID被解绑了,导致拿到的XID为空。代码类似下面这样:

try {// 调用triggerBeforeCommit()triggerBeforeCommit();// 提交事务,清除XIDtx.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 committhrow new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.CommitFailure);}
复制代码

不过经过我的一番查找,发现GlobalTransaction中是包含XID属性的,所以果断从GlobalTransaction对象中取XID传进来。

修改后的代码如下:

try {// 调用triggerBeforeCommit()triggerBeforeCommit();// 提交事务,清除XIDtx.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 committhrow new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.CommitFailure);}
复制代码

改造后的TransactionHookManager

public final class TransactionHookManager {
​private TransactionHookManager() {
​}
​private static final ThreadLocal>> LOCAL_HOOKS = new ThreadLocal<>();
​/*** get the current hooks** @return TransactionHook list*/public static List getHooks() {String xid = RootContext.getXID();return getHooks(xid);}
​/*** get hooks by xid* * @param xid* @return TransactionHook list*/public static List getHooks(String xid) {Map> hooksMap = LOCAL_HOOKS.get();if (hooksMap == null || hooksMap.isEmpty()) {return Collections.emptyList();}List hooks = new ArrayList<>();List localHooks = hooksMap.get(xid);if (StringUtils.isNotBlank(xid)) {List 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> hooksMap = LOCAL_HOOKS.get();if (hooksMap == null) {hooksMap = new HashMap<>();LOCAL_HOOKS.set(hooksMap);}String xid = RootContext.getXID();List 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> hooksMap = LOCAL_HOOKS.get();if (hooksMap == null || hooksMap.isEmpty()) {return;}hooksMap.remove(xid);if (StringUtils.isNotBlank(xid)) {hooksMap.remove(null);}}
}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
1和0什么意思网络用语 0和1... 最近,有网友给大家出了份考卷,卷面是几位“00后”的QQ对 话:dbq、xswl、nss、nbcs、...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
七大洲八大洋 七大洲简易图三角... 一、北美洲(中北美和加勒比海)北美洲面积约2471万平方公里,居世界第三。人口约5.79亿(2016...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...