• Seata AT模式TransactionHook竟然会被莫名删除!


    前言

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

    相关issue链接:github.com/seata/seata…,该用户在issue中已经指出了相关问题所在:

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

    问题定位

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

    1. try {
    2.    // 开启分布式事务,获取XID        
    3.    beginTransaction(txInfo, tx);
    4.    Object rs;
    5.    try {
    6.        // 执行业务代码
    7.        rs = business.execute();
    8.   } catch (Throwable ex) {
    9.        // 3. 处理异常,准备回滚.
    10.        completeTransactionAfterThrowing(txInfo, tx, ex);
    11.        throw ex;
    12.   }
    13.    // 4. 提交事务.
    14.    commitTransaction(tx, txInfo);
    15.    return rs;
    16. } finally {
    17.    //5. 回收现场
    18.    resumeGlobalLockConfig(previousConfig);
    19.    triggerAfterCompletion();
    20.    cleanUp();
    21. }
    22. 复制代码

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

    1. public final class TransactionHookManager {
    2.  
    3.  private static final ThreadLocal> LOCAL_HOOKS = new ThreadLocal<>();
    4.  
    5.  // 注册TransactionHook
    6.  public static void registerHook(TransactionHook transactionHook) {
    7.      if (transactionHook == null) {
    8.            throw new NullPointerException("transactionHook must not be null");
    9.       }
    10.        List transactionHooks = LOCAL_HOOKS.get();
    11.        if (transactionHooks == null) {
    12.            LOCAL_HOOKS.set(new ArrayList<>());
    13.       }
    14.        LOCAL_HOOKS.get().add(transactionHook);
    15.   }
    16.  
    17.  // 移除当前线程上所有TransactionHook
    18.  public static void clear() {
    19.      LOCAL_HOOKS.remove();
    20. }
    21. }
    22. 复制代码

    由上面的源码可知,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,因为有这么一段代码:

      1.    private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException {
      2.        try {
      3.            // 执行triggerBeforeBegin()
      4.            triggerBeforeBegin();
      5.            // 注册分布式事务,生成XID
      6.            tx.begin(txInfo.getTimeOut(), txInfo.getName());
      7.            // 执行triggerAfterBegin()
      8.            triggerAfterBegin();
      9.       } catch (TransactionException txe) {
      10.            throw new TransactionalExecutor.ExecutionException(tx, txe,
      11.                    TransactionalExecutor.Code.BeginFailure);
      12.       }
      13.   }
      14. 复制代码

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

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

    注意事项

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

    1. try {
    2.            // 调用triggerBeforeCommit()
    3.            triggerBeforeCommit();
    4.            // 提交事务,清除XID
    5.            tx.commit();
    6.            if (Arrays.asList(GlobalStatus.TimeoutRollbacking, GlobalStatus.TimeoutRollbacked).contains(tx.getLocalStatus())) {
    7.                throw new TransactionalExecutor.ExecutionException(tx,
    8.                        new TimeoutException(String.format("Global transaction[%s] is timeout and will be rollback[TC].", tx.getXid())),
    9.                        TransactionalExecutor.Code.TimeoutRollback);
    10.           }
    11.            // 调用triggerAfterCommit()
    12.            triggerAfterCommit();
    13.       } catch (TransactionException txe) {
    14.            // 4.1 Failed to commit
    15.            throw new TransactionalExecutor.ExecutionException(tx, txe,
    16.                    TransactionalExecutor.Code.CommitFailure);
    17.       }
    18. 复制代码

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

    修改后的代码如下:

    1. try {
    2.            // 调用triggerBeforeCommit()
    3.            triggerBeforeCommit();
    4.            // 提交事务,清除XID
    5.            tx.commit();
    6.            if (Arrays.asList(GlobalStatus.TimeoutRollbacking, GlobalStatus.TimeoutRollbacked).contains(tx.getLocalStatus())) {
    7.                throw new TransactionalExecutor.ExecutionException(tx,
    8.                        new TimeoutException(String.format("Global transaction[%s] is timeout and will be rollback[TC].", tx.getXid())),
    9.                        TransactionalExecutor.Code.TimeoutRollback);
    10.           }
    11.            // 调用triggerAfterCommit()
    12.            triggerAfterCommit(tx.getXid());
    13.       } catch (TransactionException txe) {
    14.            // 4.1 Failed to commit
    15.            throw new TransactionalExecutor.ExecutionException(tx, txe,
    16.                    TransactionalExecutor.Code.CommitFailure);
    17.       }
    18. 复制代码

    改造后的TransactionHookManager

    1. public final class TransactionHookManager {
    2.    private TransactionHookManager() {
    3.   }
    4.    private static final ThreadLocal<Map<String, List<TransactionHook>>> LOCAL_HOOKS = new ThreadLocal<>();
    5.    /**
    6.     * get the current hooks
    7.     *
    8.     * @return TransactionHook list
    9.     */
    10.    public static List<TransactionHook> getHooks() {
    11.        String xid = RootContext.getXID();
    12.        return getHooks(xid);
    13.   }
    14.    /**
    15.     * get hooks by xid
    16.     *
    17.     * @param xid
    18.     * @return TransactionHook list
    19.     */
    20.    public static List<TransactionHook> getHooks(String xid) {
    21.        Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
    22.        if (hooksMap == null || hooksMap.isEmpty()) {
    23.            return Collections.emptyList();
    24.       }
    25.        List<TransactionHook> hooks = new ArrayList<>();
    26.        List<TransactionHook> localHooks = hooksMap.get(xid);
    27.        if (StringUtils.isNotBlank(xid)) {
    28.            List<TransactionHook> virtualHooks = hooksMap.get(null);
    29.            if (virtualHooks != null && !virtualHooks.isEmpty()) {
    30.                hooks.addAll(virtualHooks);
    31.           }
    32.       }
    33.        if (localHooks != null && !localHooks.isEmpty()) {
    34.            hooks.addAll(localHooks);
    35.       }
    36.        if (hooks.isEmpty()) {
    37.            return Collections.emptyList();
    38.       }
    39.        return Collections.unmodifiableList(hooks);
    40.   }
    41.    /**
    42.     * add new hook
    43.     *
    44.     * @param transactionHook transactionHook
    45.     */
    46.    public static void registerHook(TransactionHook transactionHook) {
    47.        if (transactionHook == null) {
    48.            throw new NullPointerException("transactionHook must not be null");
    49.       }
    50.        Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
    51.        if (hooksMap == null) {
    52.            hooksMap = new HashMap<>();
    53.            LOCAL_HOOKS.set(hooksMap);
    54.       }
    55.        String xid = RootContext.getXID();
    56.        List<TransactionHook> hooks = hooksMap.get(xid);
    57.        if (hooks == null) {
    58.            hooks = new ArrayList<>();
    59.            hooksMap.put(xid, hooks);
    60.       }
    61.        hooks.add(transactionHook);
    62.   }
    63.    /**
    64.     * clear hooks by xid
    65.     *
    66.     * @param xid
    67.     */
    68.    public static void clear(String xid) {
    69.        Map<String, List<TransactionHook>> hooksMap = LOCAL_HOOKS.get();
    70.        if (hooksMap == null || hooksMap.isEmpty()) {
    71.            return;
    72.       }
    73.        hooksMap.remove(xid);
    74.        if (StringUtils.isNotBlank(xid)) {
    75.            hooksMap.remove(null);
    76.       }
    77.   }
    78. }
  • 相关阅读:
    Java开发中如何配合MySQL实现读写分离?
    leetCode 70.爬楼梯 动态规划
    Vue前端开发:事件传参
    什么是Java中的“内存屏障“(Memory Barrier)?它们有什么作用?
    【数字人】3、LIA | 使用隐式空间来实现视频驱动单张图数字人生成(ICLR 2022)
    六、回归与聚类算法 - 模型保存与加载
    一文搞懂APT攻击
    Python爬虫——JsonPath解析方式
    Node.js学习篇(一)利用fs引入文件和写入或修改文件以及path
    基于sklearn的机器学习实战
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/127864239