Seata分布式事务出现ABA问题解决

 更新时间:2022年11月09日 16:26:13   作者:梦想实现家_Z  
这篇文章主要为大家介绍了Seata分布式事务出现ABA问题解决方法示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

兄弟们,最近处理了一个seata的issue,关于seata分布式事务长期回滚失败后,突然回滚成功了:

这个问题的出现需要以下两个契机:

  • 在执行分布式事务期间,有本地事务与分布式事务操作同一张表中的数据导致脏写产生;
  • 在回滚时,seata对比afterImage与当前数据不一致,导致回滚失败,此时会一直重试;
  • 当手工校准数据后,某一时刻afterImage与当前数据一致,此时回滚重试成功,ABA问题产生;

从源码中定位原因

为了避免ABA问题的产生,通过与seata社区的大佬讨论,最终决定在回滚时,如果对比afterImage与当前数据不一致的情况下,不再尝试回滚重试。这样的话,即使后续通过人工校准后,也不会回滚了。但是这样有另一个问题,就是人工校准后,这个分布式事务就一直遗留在数据库中无法删除了。针对这个问题,seata应该要提供一个restful api让开发人员在数据校准后能够删除掉对应的分布式事务数据。

在seata源码中,如果校验afterImage与当前数据不一致后,会抛出SQLException,最终会被上层代码捕获包装成BranchTransactionException异常,但是里面的code属性是BranchRollbackFailed_Retriable,这也是导致seata一直重试回滚的根本原因:

Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
        if (!afterEqualsCurrentResult.getResult()) {
            // 先比较afterImage与当前数据,如果不一致,那么再比较当前数据和beforeImage是否一致
            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
            // 如果当前数据和beforeImage一致,那么不需要回滚了,因为相当于已经回滚了
            if (beforeEqualsCurrentResult.getResult()) {
                if (LOGGER.isInfoEnabled()) {
                    LOGGER.info("Stop rollback because there is no data change " +
                            "between the before data snapshot and the current data snapshot.");
                }
                // no need continue undo.
                return false;
            } else {
                // 否则,直接抛出SQLException,并告知undo log脏写了
                if (LOGGER.isInfoEnabled()) {
                    if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) {
                        LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams());
                    }
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("check dirty data failed, old and new data are not equal, " +
                            "tableName:[" + sqlUndoLog.getTableName() + "]," +
                            "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," +
                            "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "].");
                }
                throw new SQLException("Has dirty records when undo.");
            }
        }

在上层调用代码中,我们可以找到这样一段:

catch (Throwable e) {
    if (conn != null) {
        try {
            conn.rollback();
        } catch (SQLException rollbackEx) {
            LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
        }
    }
    // 包装异常
    throw new BranchTransactionException(BranchRollbackFailed_Retriable, String
                    .format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid,
                        branchId, e.getMessage()), e);
}

根据源码分析,我们发现在数据校验后抛出的SQLException会被包装成code属性为BranchRollbackFailed_RetriableBranchTransactionException异常,这样会导致seata不断重试回滚操作。

如何处理

我们需要将这个SQLException调整为一个更加具体的异常,比如SQLUndoDirtyException这种能够明确地表示undo log被脏写的异常,另外我们在上层代码中同样需要针对SQLUndoDirtyException做特殊处理,比如包装成new BranchTransactionException(BranchRollbackFailed_Unretriable)不可重试的状态。

先创建自定义的异常:SQLUndoDirtyException

import java.io.Serializable;
import java.sql.SQLException;
/**
 * @author zouwei
 */
class SQLUndoDirtyException extends SQLException implements Serializable {
    private static final long serialVersionUID = -5168905669539637570L;
    SQLUndoDirtyException(String reason) {
        super(reason);
    }
}

调整SQLExceptionSQLUndoDirtyException:

Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
        if (!afterEqualsCurrentResult.getResult()) {
            // 先比较afterImage与当前数据,如果不一致,那么再比较当前数据和beforeImage是否一致
            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
            // 如果当前数据和beforeImage一致,那么不需要回滚了,因为相当于已经回滚了
            if (beforeEqualsCurrentResult.getResult()) {
                if (LOGGER.isInfoEnabled()) {
                    LOGGER.info("Stop rollback because there is no data change " +
                            "between the before data snapshot and the current data snapshot.");
                }
                // no need continue undo.
                return false;
            } else {
                // 否则,直接抛出SQLException,并告知undo log脏写了
                if (LOGGER.isInfoEnabled()) {
                    if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) {
                        LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams());
                    }
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("check dirty data failed, old and new data are not equal, " +
                            "tableName:[" + sqlUndoLog.getTableName() + "]," +
                            "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," +
                            "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "].");
                }
                // 替换为具体的SQLUndoDirtyException异常
                throw new SQLUndoDirtyException("Has dirty records when undo.");
            }
        }

这样的话,我们在上层代码中,就可以针对性地处理了:

catch (Throwable e) {
    if (conn != null) {
        try {
            conn.rollback();
        } catch (SQLException rollbackEx) {
            LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
        }
     }
     // 如果捕捉的异常为SQLUndoDirtyException,那么包装为BranchRollbackFailed_Unretriable
     if (e instanceof SQLUndoDirtyException) {
         throw new BranchTransactionException(BranchRollbackFailed_Unretriable, String.format(
                        "Branch session rollback failed because of dirty undo log, please delete the relevant undolog after manually calibrating the data. xid = %s branchId = %s",
                        xid, branchId), e);
      }
      throw new BranchTransactionException(BranchRollbackFailed_Retriable,
                    String.format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid,
                        branchId, e.getMessage()),
                    e);
}

我们在上层调用代码中捕捉指定的SQLUndoDirtyException,直接包装为BranchRollbackFailed_Unretriable状态的BranchTransactionException,这样我们的分布式事务就不会一直重试回滚操作了。

下一步就需要开发人员人工介入校准数据后删除对应的undo log,在一系列操作处理完毕后,另外还需要seata tc端提供对应的restful api开放对应的手工触发回滚的操作,以便保证校准后的分布式事务正常结束。

小结

我们根据seata使用人员反馈的问题,通过源码分析找到了造成问题的原因:

  • 开发人员在使用seata的时候,对于同一张表的操作没有使用@GlobalTransactional注解覆盖到,导致了undo log被脏写;
  • 当产生回滚时,在进行数据校验时,发现afterImage与当前数据不一致进而无法正常回滚,抛出SQLException,最终包装成BranchRollbackFailed_Retriable异常,导致seata一直重试回滚;
  • 在数据校准后,某一刻的数据与afterImage一致,此时seata就回滚成功,形成ABA问题;

该pr将在1.6版本后解决seata分布式事务一直尝试回滚的问题,可以避免ABA问题的产生,后续还需要提供一些其他功能辅助开发人员回滚数据。

以上就是Seata分布式事务出现ABA问题解决的详细内容,更多关于Seata分布式事务ABA的资料请关注脚本之家其它相关文章!

相关文章

  • Spring的CorsFilter会失效的原因及解决方法

    Spring的CorsFilter会失效的原因及解决方法

    众所周知CorsFilter是Spring提供的跨域过滤器,我们可能会做以下的配置,基本上就是允许任何跨域请求,我利用Spring的CorsFilter做跨域操作但是出现报错,接下来小编就给大家介绍一Spring的CorsFilter会失效的原因及解决方法,需要的朋友可以参考下
    2023-09-09
  • SpringBoot 自动装配的原理详解分析

    SpringBoot 自动装配的原理详解分析

    这篇文章主要介绍了SpringBoot 自动装配的原理详解分析,文章通过通过一个案例来看一下自动装配的效果展开详情,感兴趣的小伙伴可以参考一下
    2022-08-08
  • Java使用递归解决算法问题的实例讲解

    Java使用递归解决算法问题的实例讲解

    递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解,这里我们就来看几个Java使用递归解决算法问题的实例讲解
    2016-06-06
  • java构造方法的作用总结

    java构造方法的作用总结

    在本篇文章里小编给大家整理了关于java构造方法的相关知识点以及实例代码,有需要的朋友们可以学习下。
    2019-07-07
  • Java利用InputStream类实现文件读取与处理

    Java利用InputStream类实现文件读取与处理

    在Java开发中,输入流(InputStream)是一个非常重要的概念,它涉及到文件读写、网络传输等多个方面,InputStream类是Java中输入流的抽象基类,定义了读取输入流数据的方法,本文将以InputStream类为切入点,介绍Java中的输入流概念及其应用,需要的朋友可以参考下
    2023-11-11
  • java 使用Graphics2D在图片上写字

    java 使用Graphics2D在图片上写字

    这篇文章主要介绍了java 使用Graphics2D在图片上写字,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java Bean 作用域及它的几种类型介绍

    Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧
    2022-09-09
  • JPA使用乐观锁应对高并发方式

    JPA使用乐观锁应对高并发方式

    这篇文章主要介绍了JPA使用乐观锁应对高并发方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • java多线程从入门到精通看这篇就够了

    java多线程从入门到精通看这篇就够了

    熟悉 Java 多线程编程的同学都知道,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了,今天通过本文给大家分享java多线程从入门到精通的相关知识,一起看看吧
    2021-06-06
  • springboot整合shiro与自定义过滤器的全过程

    springboot整合shiro与自定义过滤器的全过程

    这篇文章主要给大家介绍了关于springboot整合shiro与自定义过滤器以及Shiro中权限控制的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-01-01

最新评论