• 想不到吧,Seata分布式事务也会出现ABA问题


    前言

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

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

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

    从源码中定位原因

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

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

    1. Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
    2.        if (!afterEqualsCurrentResult.getResult()) {
    3.            // 先比较afterImage与当前数据,如果不一致,那么再比较当前数据和beforeImage是否一致
    4.            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
    5.            // 如果当前数据和beforeImage一致,那么不需要回滚了,因为相当于已经回滚了
    6.            if (beforeEqualsCurrentResult.getResult()) {
    7.                if (LOGGER.isInfoEnabled()) {
    8.                    LOGGER.info("Stop rollback because there is no data change " +
    9.                            "between the before data snapshot and the current data snapshot.");
    10.               }
    11.                // no need continue undo.
    12.                return false;
    13.           } else {
    14.                // 否则,直接抛出SQLException,并告知undo log脏写了
    15.                if (LOGGER.isInfoEnabled()) {
    16.                    if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) {
    17.                        LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams());
    18.                   }
    19.               }
    20.                if (LOGGER.isDebugEnabled()) {
    21.                    LOGGER.debug("check dirty data failed, old and new data are not equal, " +
    22.                            "tableName:[" + sqlUndoLog.getTableName() + "]," +
    23.                            "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," +
    24.                            "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "].");
    25.               }
    26.                throw new SQLException("Has dirty records when undo.");
    27.           }
    28.       }
    29. 复制代码

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

    1. catch (Throwable e) {
    2.    if (conn != null) {
    3.        try {
    4.            conn.rollback();
    5.       } catch (SQLException rollbackEx) {
    6.            LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
    7.       }
    8.   }
    9.    // 包装异常
    10.    throw new BranchTransactionException(BranchRollbackFailed_Retriable, String
    11.                   .format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid,
    12.                        branchId, e.getMessage()), e);
    13. }
    14. 复制代码

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

    如何处理

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

    先创建自定义的异常:SQLUndoDirtyException

    1. import java.io.Serializable;
    2. import java.sql.SQLException;
    3. /**
    4. * @author zouwei
    5. */
    6. class SQLUndoDirtyException extends SQLException implements Serializable {
    7.    private static final long serialVersionUID = -5168905669539637570L;
    8.    SQLUndoDirtyException(String reason) {
    9.        super(reason);
    10.   }
    11. }
    12. 复制代码

    调整SQLExceptionSQLUndoDirtyException:

    1. Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
    2.        if (!afterEqualsCurrentResult.getResult()) {
    3.            // 先比较afterImage与当前数据,如果不一致,那么再比较当前数据和beforeImage是否一致
    4.            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
    5.            // 如果当前数据和beforeImage一致,那么不需要回滚了,因为相当于已经回滚了
    6.            if (beforeEqualsCurrentResult.getResult()) {
    7.                if (LOGGER.isInfoEnabled()) {
    8.                    LOGGER.info("Stop rollback because there is no data change " +
    9.                            "between the before data snapshot and the current data snapshot.");
    10.               }
    11.                // no need continue undo.
    12.                return false;
    13.           } else {
    14.                // 否则,直接抛出SQLException,并告知undo log脏写了
    15.                if (LOGGER.isInfoEnabled()) {
    16.                    if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) {
    17.                        LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams());
    18.                   }
    19.               }
    20.                if (LOGGER.isDebugEnabled()) {
    21.                    LOGGER.debug("check dirty data failed, old and new data are not equal, " +
    22.                            "tableName:[" + sqlUndoLog.getTableName() + "]," +
    23.                            "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," +
    24.                            "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "].");
    25.               }
    26.                // 替换为具体的SQLUndoDirtyException异常
    27.                throw new SQLUndoDirtyException("Has dirty records when undo.");
    28.           }
    29.       }
    30. 复制代码

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

    1. catch (Throwable e) {
    2.    if (conn != null) {
    3.        try {
    4.            conn.rollback();
    5.       } catch (SQLException rollbackEx) {
    6.            LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
    7.       }
    8.     }
    9.     // 如果捕捉的异常为SQLUndoDirtyException,那么包装为BranchRollbackFailed_Unretriable
    10.     if (e instanceof SQLUndoDirtyException) {
    11.         throw new BranchTransactionException(BranchRollbackFailed_Unretriable, String.format(
    12.                        "Branch session rollback failed because of dirty undo log, please delete the relevant undolog after manually calibrating the data. xid = %s branchId = %s",
    13.                        xid, branchId), e);
    14.     }
    15.      throw new BranchTransactionException(BranchRollbackFailed_Retriable,
    16.                    String.format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid,
    17.                        branchId, e.getMessage()),
    18.                    e);
    19. }
    20. 复制代码

    我们在上层调用代码中捕捉指定的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问题的产生,后续还需要提供一些其他功能辅助开发人员回滚数据。

     

  • 相关阅读:
    五、伊森商城 前端基础-Vue v-text&v-html&v-bind&v-model p23
    Python:对程序做性能分析及计时统计
    Java JSON字符串转换成JSON对象
    Zabbix邮箱报警
    Notpad-- ubuntu下载安装
    【工具类】阿里域名关联ip(python版)
    AJAX的使用,搭建web服务器,AJAX响应消息类型,JSON
    怎么给图片添加贴纸?介绍几个简单的方法
    在 Elasticsearch 中实现自动完成功能 1:Prefix queries
    Python-基础知识汇集
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/127767951