人人都会AI编程

10.4 回滚规则:异常类型、手动回滚、部分回滚

更新时间:2026-07-10

事务管理的核心价值之一是保证数据的一致性,而回滚策略决定了当方法执行过程中出现问题时,哪些变更应该被撤销。Spring 提供了灵活而实用的回滚控制机制:默认基于运行时异常自动回滚,支持声明式和编程式的手动回滚,甚至可以通过保存点实现部分回滚。掌握这些规则,能够让你在复杂的业务场景中精确控制事务行为。

10.4.1 默认回滚规则:异常类型的判断

在 Spring 的声明式事务管理中,回滚的触发条件与抛出的异常类型直接相关。默认规则是:

  • 自动回滚:当事务方法抛出 RuntimeException 或其子类(即非受检异常)时,事务将自动回滚。
  • 不回滚:当事务方法抛出 Exception 的子类但并非 RuntimeException(即受检异常,如 IOExceptionSQLException)时,事务不会自动回滚,会照常提交。

这一默认行为源于 Spring 设计者的实践洞察:受检异常通常代表可预见的业务异常或可恢复的例外情况,调用方理应捕获并处理,此时提交事务或许是合理的;而非受检异常往往由编程错误或不可恢复的故障引起,回滚是保证数据安全的基本要求。

例如,以下方法抛出 IllegalArgumentException(非受检),事务会自动回滚:

@Transactional
public void transferMoney(Long from, Long to, BigDecimal amount) {
    accountRepository.debit(from, amount);
    if (amount.compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("转账金额必须大于零");  // 非受检异常,触发回滚
    }
    accountRepository.credit(to, amount);
}

如果抛出的是自定义的受检异常,比如 InsufficientFundsException 继承自 Exception,则默认不会回滚,数据库变更会提交。这种行为在一些业务场景中可能并不符合预期。

定制回滚异常

你可以通过 @Transactional 注解的 rollbackFornoRollbackFor 属性,显式指定哪些异常类型需要回滚,哪些不需要:

@Transactional(rollbackFor = {BusinessException.class, SQLException.class})
public void processOrder(Order order) throws BusinessException, SQLException {
    // 当抛出 BusinessException 或 SQLException 时回滚
}

@Transactional(noRollbackFor = {ValidationException.class})
public void updateProfile(UserProfile profile) {
    // 参数校验失败抛出 ValidationException 时,不回滚,记录失败日志后继续
}

rollbackFornoRollbackFor 接受异常类的数组,可以同时配置多个。通常建议:

  • 将不可恢复的业务异常配置为回滚:如余额不足、库存缺货等,这些异常意味着整个操作链无法继续,应回滚所有操作。
  • 将可忽略的检查异常配置为不回滚:如发送通知失败、记录操作日志失败,这些辅助操作失败不应影响主流程数据的一致性。

如果采用编程式事务管理(使用 TransactionTemplatePlatformTransactionManager),你可以直接在代码中调用 setRollbackOnly() 方法标记事务需要回滚,异常类型规则仍然有效,但回滚决策可以结合更复杂的业务判断。

10.4.2 手动回滚:主动控制事务命运

在某些场景下,不能仅凭抛出的异常类型决定是否回滚,而需要在业务逻辑中主动判断并标记回滚,同时返回一个正常的结果或执行其他补偿操作。Spring 提供了两种手动回滚的方式。

方式一:通过 TransactionAspectSupport 标记回滚

在声明式事务管理的方法内,可以使用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 将当前事务标记为回滚。此后无论方法是否正常结束,Spring 在事务提交前都会检测该标记,并执行回滚。

@Transactional
public Result transfer(Long from, Long to, BigDecimal amount) {
    accountRepository.debit(from, amount);
    boolean creditSuccess = accountRepository.credit(to, amount);

    if (!creditSuccess) {
        // 主动标记回滚,但仍然返回一个结果给调用方
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return Result.fail("入账失败,交易已回滚");
    }
    return Result.success("转账成功");
}

这个技巧常用于需要返回业务状态给前端,但必须回滚数据库操作的场景,例如余额不足时让用户看到明确的错误信息,同时确保扣款被撤销。

方式二:编程式事务管理中的手动回滚

如果使用 TransactionTemplate,可以直接通过回调的 TransactionStatus 参数控制回滚:

public Result transfer(Long from, Long to, BigDecimal amount) {
    return transactionTemplate.execute(status -> {
        accountRepository.debit(from, amount);
        boolean creditSuccess = accountRepository.credit(to, amount);
        if (!creditSuccess) {
            status.setRollbackOnly();    // 标记回滚
            return Result.fail("入账失败");
        }
        return Result.success("转账成功");
    });
}

编程式管理让你拥有完全的控制力,可以结合任意复杂的业务条件决定回滚,甚至在回滚前执行额外的清理操作。

10.4.3 部分回滚:保存点与嵌套事务

默认情况下,事务回滚会撤销该事务内的所有操作。但在一些复杂业务中,你可能希望只回滚事务中的一部分操作,而保留其他操作的结果。例如,批量导入数据时,某一条记录因校验失败需要回滚,但其他成功的记录应当正常提交。

Spring 支持通过保存点(Savepoint) 实现部分回滚,前提是底层事务管理器支持保存点(大多数 JDBC 数据源都支持)。Spring 的 TransactionStatus 提供了一个 createSavepoint() 方法和 rollbackToSavepoint(Object savepoint) 方法,可用于编程式事务管理。

public void batchProcess(List<Record> records) {
    transactionTemplate.execute(status -> {
        for (Record record : records) {
            Object sp = status.createSavepoint();   // 设置保存点
            try {
                processSingleRecord(record);
            } catch (BusinessException e) {
                status.rollbackToSavepoint(sp);    // 回滚到保存点,只撤销本条记录
                log.warn("处理记录失败,已回滚单条:{}", record.getId());
            }
        }
        return null; // 所有记录处理完,提交事务
    });
}

在这个例子中,batchProcess 整体在一个大事务内。循环中遇到某条记录处理失败时,只回滚到该条记录处理前创建的那个保存点,不会影响已经成功处理的其它记录。循环退出后,事务整体提交,保留所有成功记录的变更。

注意事项

  • 声明式事务不支持保存点@Transactional 无法声明保存点,必须使用编程式管理。
  • 部分回滚的适用场景有限:它适用于批量处理中需要独立撤销部分操作的场合。在强一致性要求的业务(如资金转账)中不宜使用,因为部分提交可能导致数据不一致的中间状态。
  • 嵌套事务(Propagation.NESTED):在支持保存点的环境中,Propagation.NESTED 会创建一个嵌套事务,底层就是利用保存点实现。当嵌套子事务回滚时,只回滚子事务内的操作,外部事务可以继续提交。但注意嵌套事务并不是完全的独立事务,其提交必须依赖于外部事务的提交。
@Transactional
public void outerMethod() {
    accountRepository.debit(from, 100);
    try {
        innerService.nestedMethod();  // 该方法标注了 @Transactional(propagation = Propagation.NESTED)
    } catch (RuntimeException e) {
        // nestedMethod 抛出异常,只会回滚它内部的操作,外部 debit 不受影响
    }
    accountRepository.credit(to, 100); // 外部操作继续
}

10.4.4 实践中的建议

  1. 明确异常分类:将业务异常设计为符合回滚预期的继承结构,例如定义 BusinessRollbackException extends RuntimeException 用于那些必须回滚的通用业务错误,避免每次都配置 rollbackFor
  2. 谨慎使用受检异常携带回滚语义:如果你习惯用受检异常表达可恢复错误,一定要记得配置 rollbackFor,否则容易出现不回滚的意外。
  3. 手动回滚时注意返回值setRollbackOnly() 只是标记,方法结束后事务才会真正回滚。确保方法返回值能清晰地反映操作结果,便于调用方处理。
  4. 保存点是双刃剑:它可以提升批量处理的灵活性,但也增大了事务持有时间和锁范围,可能导致死锁或性能问题。在没有明确收益的情况下,优先考虑更小的独立事务。

回滚规则看似简单,实则是事务管理中最容易出错的环节。理解异常类型、手动干预和部分回滚的机制,能够帮助你在面对复杂业务时准确控制数据状态,而不会因为“默认行为”产生隐蔽的 bug。在下一节中,我们将进一步探讨事务的隔离级别与传播行为,从而构筑完整的事务知识体系。