事务管理的核心价值之一是保证数据的一致性,而回滚策略决定了当方法执行过程中出现问题时,哪些变更应该被撤销。Spring 提供了灵活而实用的回滚控制机制:默认基于运行时异常自动回滚,支持声明式和编程式的手动回滚,甚至可以通过保存点实现部分回滚。掌握这些规则,能够让你在复杂的业务场景中精确控制事务行为。
10.4.1 默认回滚规则:异常类型的判断
在 Spring 的声明式事务管理中,回滚的触发条件与抛出的异常类型直接相关。默认规则是:
- 自动回滚:当事务方法抛出
RuntimeException或其子类(即非受检异常)时,事务将自动回滚。 - 不回滚:当事务方法抛出
Exception的子类但并非RuntimeException(即受检异常,如IOException、SQLException)时,事务不会自动回滚,会照常提交。
这一默认行为源于 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 注解的 rollbackFor 和 noRollbackFor 属性,显式指定哪些异常类型需要回滚,哪些不需要:
@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 时,不回滚,记录失败日志后继续
}
rollbackFor 和 noRollbackFor 接受异常类的数组,可以同时配置多个。通常建议:
- 将不可恢复的业务异常配置为回滚:如余额不足、库存缺货等,这些异常意味着整个操作链无法继续,应回滚所有操作。
- 将可忽略的检查异常配置为不回滚:如发送通知失败、记录操作日志失败,这些辅助操作失败不应影响主流程数据的一致性。
如果采用编程式事务管理(使用 TransactionTemplate 或 PlatformTransactionManager),你可以直接在代码中调用 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 实践中的建议
- 明确异常分类:将业务异常设计为符合回滚预期的继承结构,例如定义
BusinessRollbackException extends RuntimeException用于那些必须回滚的通用业务错误,避免每次都配置rollbackFor。 - 谨慎使用受检异常携带回滚语义:如果你习惯用受检异常表达可恢复错误,一定要记得配置
rollbackFor,否则容易出现不回滚的意外。 - 手动回滚时注意返回值:
setRollbackOnly()只是标记,方法结束后事务才会真正回滚。确保方法返回值能清晰地反映操作结果,便于调用方处理。 - 保存点是双刃剑:它可以提升批量处理的灵活性,但也增大了事务持有时间和锁范围,可能导致死锁或性能问题。在没有明确收益的情况下,优先考虑更小的独立事务。
回滚规则看似简单,实则是事务管理中最容易出错的环节。理解异常类型、手动干预和部分回滚的机制,能够帮助你在面对复杂业务时准确控制数据状态,而不会因为“默认行为”产生隐蔽的 bug。在下一节中,我们将进一步探讨事务的隔离级别与传播行为,从而构筑完整的事务知识体系。