Seata AT模式TransactionHook会被莫名删除?

news2025/7/22 21:54:46

前言

兄弟们,刚刚又给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<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()回收现场时,把该线程当中存储的所有TransactionHook全部清除掉了,导致事务A的生命周期中找不到该事务对应的TransactionHook,从而产生了BUG

如何解决

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

1.改造TransactionHookManager.LOCAL_HOOKS,把数据类型改成ThreadLocal<Map<String, List<TransactionHook>>>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();
                // 注册分布式事务,生成XID
                tx.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();
            // 提交事务,清除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);
        }
复制代码

不过经过我的一番查找,发现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);
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最新JMeter面试题,紧扣面试实际要求,看完拿下20K

JMeter是一款纯java编写负载功能测试和性能测试开源工具软件。相比Loadrunner而言&#xff0c;JMeter小巧轻便且免费&#xff0c;逐渐成为了主流的性能测试工具&#xff0c;是测试人员必须要掌握的工具之一。 下面小编给大家总结了22年最新30道关于JMeter的面试题&#xff0c;…

基于51单片机的温度报警系统

功能&#xff1a; 设计一个温度报警系统&#xff0c;可以设定温度上、下限的值&#xff0c;到达设定值时&#xff0c;蜂鸣器响&#xff1b;按键设定有设置、确定、取消、减少与增加功能&#xff0c;LCD1602实时显示相关温度信息 设定界面&#xff1a; 温度超过设定值&#xf…

美国能源部国家实验室将量子计算用于关键能源研究

​ &#xff08;图片来源&#xff1a;网络&#xff09; 量子计算是一种新兴的、强大而有前途的且能快速解决复杂问题的新力量&#xff0c;美国能源部国家实验室NETL的专家正准备将量子计算投入到关键能源的研究课题上&#xff0c;以实现环境可持续和能源应用的无限未来。 量子…

【MySQL | 运维篇】05、MySQL 分库分表之 使用 MyCat 分片

目录 一、垂直拆分 1.1 场景 1.2 准备 1.3 配置 1). schema.xml 2). server.xml 1.4 测试 1). 上传测试SQL脚本到服务器的 /root/sql 目录 2). 执行指令导入测试数据 3). 查询用户的收件人及收件人地址信息(包含省、市、区)。 4). 查询每一笔订单及订单的收件地址信息…

html页面广告5秒之后跳过

首页 - <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width; in…

【51】分布式计算:如果所有人的大脑都联网会怎样?

【计算机组成原理】学习笔记——总目录 【51】分布式计算&#xff1a;如果所有人的大脑都联网会怎样&#xff1f;引言一、从硬件升级到水平扩展二、理解高可用性和单点故障三、总结【个人总结的重点】引言 现在我们每天在用的个人 PC、智能手机&#xff0c;乃至云上的服务器&a…

Nginx反向代理配置

关键字&#xff1a; 反向代理&#xff0c;负载均衡 第一步&#xff1a;官网下载windwos版本nginx 下载地址链接:nginx: download 如下图所示 第二步&#xff1a;解压启动nginx 备注&#xff1a;启动前先查看进程是否占用 80端口 netstat ano | findstr 80tasklist |findstr “…

linux下Nerdtree安装方法

目录 1.下载Nerdtree 2. linux下安装 3. 成功享受吧 1.下载Nerdtree 百度网盘下载&#xff0c;地址为链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;07e3 --来自百度网盘超级会员V4的分享 github方式下载&#xff0c;地址为 https://github.com/scrooloose/ner…

【附源码】Python计算机毕业设计蔬果批发网络平台

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

HECTF2022 学习笔记

HECTF2022 MISC笔记 目录HECTF2022 MISC笔记小鲨鱼来喽~舞者的秘密你把我的flag藏哪去了。。小鲨鱼来喽~ 查看最后面的http数据包&#xff0c;发现flag 舞者的秘密 先爆破出压缩包密码&#xff0c;用010打开&#xff0c;发现文件开头是gif的格式 将图片格式改为gif&#xf…

Mysql之常用函数、聚合函数合并(unionunion all)【第四篇】

大纲&#xff1a; 1、函数的简介 MySQL函数是 MySQL 数据库提供的内部函数&#xff0c;这些内部函数可以帮助用户更加方便地处理表中的数据。函数就像预定的公式一样存放在数据库里&#xff0c;每个用户都可以调用已经存在的函数来完成某些功能。 简单来说&#xff0c;函数就是…

Sublime Text v4.0(4143)破解方法

[TOC](Sublime Text v4.0(4143)破解方法) 版本Sublime Text v4.0(4143) 所需软件 Sublime Text v4.0(4143)下载地址&#xff1a;https://www.sublimetext.com/download_thanks?targetwin-x64 010 Editor下载地址(其他十六进制编辑器也可以) https://download.sweetscape.co…

基于ISO13209(OTX)实现EOL下线序列

一 OTX是什么&#xff1f; OTX&#xff0c;全称Open Test sequence eXchange format&#xff0c;即开放式测试序列交换格式&#xff0c;国际标准&#xff1a;ISO13209&#xff0c;是专为汽车行业制定的序列开发标准。在车辆诊断、自动化标定和ECU测试等领域有广泛应用。OTX不仅…

使用Jmeter进行性能测试的操作方法

前言 JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c; 下载地址&#xff1a; Apache JMeter - Download Apache JMeter 下载好之后 &#xff0c;他得需要 jdk配置 好环境变量 才能运行。 这是很重要的一点&#xff0c; 。 安装 一系…

高数 |【23数一 李林六套卷】卷二 自用思路 及 知识点 整理

23数一 李林六套卷 —— 自用思路 及 知识点 整理 ——【卷二】 以下均为个人复盘。 部分思路讲解参考于 6-2_哔哩哔哩_bilibili 第二套T22_哔哩哔哩_bilibili T1:泰勒 ※ T2:高阶导 想泰勒展开 或 本题画图 法一:泰勒 法二:画图

哈夫曼树及其应用

一、基本概念 给定N个权值作为N个叶子结点&#xff0c;构造一棵二叉树&#xff0c;若该树的带权路径长度达到最小&#xff0c;称这样的二叉树为最优二叉树&#xff0c;也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树&#xff0c;权值较大的结点离根较近。 ——…

Web前端:如何提高React原生应用性能

React Native拥有大量追随者&#xff0c;从财富500强公司到新的创业公司。开发人员可以使用React Native为IOS和Android上的移动应用程序创建出色的移动UI。 随着React Native的一切进展顺利&#xff0c;它甚至有负面影响吗?是的&#xff0c;确实如此。这是React Native应用程…

整形提升和算术转换

表达式求值 表达式求值的一部分由符号的优先级和结合性决定。 同时&#xff0c;表达式求值一部分也与数据类型的转换有关。 文章目录1.隐式类型转换2.算术转换1.隐式类型转换 C的整数类型运算总是至少以缺省整形类型的精度来进行的。&#xff08;缺省就是如果程序员没定义函数…

【学习日志】2022.11.11 合同矩阵、惯性指数、委托构造、继承控制、=delete、可变参数模板类

class Info { public:Info() : Info(1) { } // 委托构造函数Info(int i) : Info(i, a) { } // 既是目标构造函数&#xff0c;也是委托构造函数Info(char e): Info(1, e) { }private:Info(int i, char e): type(i), name(e) { /* 其它初始化 */ } // 目标构造函数int type;c…

UD4KB100-ASEMI智能家居专用整流桥UD4KB100

编辑-Z UD4KB100在D3K封装里采用的4个芯片&#xff0c;其尺寸都是72MIL&#xff0c;是一款智能家居专用整流桥。UD4KB100的浪涌电流Ifsm为125A&#xff0c;漏电流(Ir)为10uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。UD4KB100采用光阻GPP芯片材质&#xff0c;里面有4颗…