Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用seata2.0发现事务回滚失败,源码查看是因为seata服务端发起回滚部分代码异常,发现查询分支事务集合由原本的倒序获取改为正序获取,导致客户端回滚因脏数据失败 #6120

Closed
CerebrumWisdom opened this issue Dec 9, 2023 · 2 comments · Fixed by #6121
Assignees

Comments

@CerebrumWisdom
Copy link

Ⅰ. Issue Description

使用seata2.0发现事务回滚失败,源码查看是因为seata服务端发起回滚部分代码异常,发现查询分支事务集合由原本的倒序获取改为正序获取,导致客户端回滚因脏数据失败

Ⅱ. Describe what happened

使用seata2.0发现事务回滚失败,源码查看是因为seata服务端发起回滚部分代码异常,发现查询分支事务集合由原本的倒序获取改为正序获取,导致客户端回滚因脏数据失败,测试代码如下

   @GlobalTransactional(rollbackFor = Exception.class)
    public void savesOne(Order order) {
        log.info("开始新建订单");
        orderMapper.save(order);
        log.info("结束新建订单");

        log.info("开始调用库存");
        storageFeign.updateStorage(order.getProductId(), order.getNums());
        log.info("结束调用库存");

        log.info("开始调用账户");
        accountFeign.result(order.getUserId(), order.getMoney());
        log.info("结束调用账户");

        log.info("开始修改订单状态");
        orderMapper.update(order.getUserId(), 0);
        log.info("结束修改订单状态");
        System.out.println(1/0);
    }

看出我对于order 先进行了save操作,最后有updata操作了一次,2.0回滚失败,1.7正常
我查看源码发现如下问题:

 protected boolean dataValidationAndGoOn(ConnectionProxy conn) throws SQLException {
       // 获取前置镜像内容
        TableRecords beforeRecords = sqlUndoLog.getBeforeImage();
      // 获取后置镜像内容
        TableRecords afterRecords = sqlUndoLog.getAfterImage();

        // Compare current data with before data
        // No need undo if the before data snapshot is equivalent to the after data snapshot.
        Result<Boolean> beforeEqualsAfterResult = DataCompareUtils.isRecordsEquals(beforeRecords, afterRecords);
        if (beforeEqualsAfterResult.getResult()) {
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Stop rollback because there is no data change " +
                        "between the before data snapshot and the after data snapshot.");
            }
            // no need continue undo.
            return false;
        }

        // Validate if data is dirty.
       // 当前数据
        TableRecords currentRecords = queryCurrentRecords(conn);
        // compare with current data and after image.
        Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);
        if (!afterEqualsCurrentResult.getResult()) {

            // If current data is not equivalent to the after data, then compare the current data with the before
            // data, too. No need continue to undo if current data is equivalent to the before data snapshot
            Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords);
            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 {
                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 SQLUndoDirtyException("Has dirty records when undo.");
            }
        }
        return true;
    }

上面部分源码中到执行到下面部分源码

        TableRecords currentRecords = queryCurrentRecords(conn);
        // compare with current data and after image.
        Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords);

就会被判断为脏数据,因为2.0是正序查询分支事务,那么正序第一次执行的是save操作,那么后置镜像内容与当前数据确实是不一样的因为被update修改了,所以对比就是脏数据了,而1.7倒序就不会有这个问题,修改那次事务的后置镜像与当前数据相同,所以回滚成功。
server端源码部分:

 @Override
    public boolean doGlobalRollback(GlobalSession globalSession, boolean retrying) throws TransactionException {
        boolean success = true;
        // start rollback event
        MetricsPublisher.postSessionDoingEvent(globalSession, retrying);

        if (globalSession.isSaga()) {
            success = getCore(BranchType.SAGA).doGlobalRollback(globalSession, retrying);
        } else {
           //  1.7获取方式  倒序
            List<BranchSession> reverseSortedBranches = globalSession.getReverseSortedBranches();
          //  2.0或许方式  正序
            List<BranchSession> branchSessions = globalSession.getSortedBranches();
            Boolean result = SessionHelper.forEach(branchSessions, branchSession -> {
                BranchStatus currentBranchStatus = branchSession.getStatus();
                if (currentBranchStatus == BranchStatus.PhaseOne_Failed) {
                    SessionHelper.removeBranch(globalSession, branchSession, !retrying);
                    return CONTINUE;
                }

我不确定这是bug还是对2.0增加了其他什么操作需要正序查询分支事务

Ⅵ. Environment:

  • JDK version: 1.8
  • Seata client/server version: 2.0
  • Database version: 8.0
@q343959872
Copy link

遇到相同问题,埋个点

@funky-eyes
Copy link
Contributor

I confirm that this is a bug and I will fix it as soon as possible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants