Spring默认仅对RuntimeException及其子类和Error自动回滚,受检异常如SQLException不回滚;需用rollbackFor显式指定回滚异常,noRollbackFor排除特定异常。

Java 中事务管理与异常捕获的配合,核心在于“什么异常触发回滚、什么异常不干扰事务、什么情况下要手动干预”。Spring 默认只对 未检查异常(RuntimeException 及其子类)和 Error 自动回滚,而对受检异常(如 SQLException、IOException)默认不回滚——这不是疏忽,而是设计选择:它把“是否业务失败”的判断权交还给开发者。
默认回滚规则必须清楚
Spring 的 @Transactional 默认行为是:
- 抛出 RuntimeException 或其子类(如 NullPointerException、IllegalArgumentException、自定义的 BusinessException)→ 自动回滚
- 抛出 Error(如 OutOfMemoryError)→ 自动回滚
- 抛出受检异常(如 IOException、SQLException)→ 不回滚,事务正常提交
这意味着,如果你在 service 方法里 catch 住一个 SQLException 后又“吞掉”或仅记录日志,却没有重新抛出运行时异常,那数据库变更就会被提交——即使业务逻辑本意是失败。这是生产环境中数据不一致的常见源头。
按需声明 rollbackFor 和 noRollbackFor
用注解精准控制回滚边界:
立即学习“Java免费学习笔记(深入)”;
- @Transactional(rollbackFor = SQLException.class):显式要求 SQLException 触发回滚
- @Transactional(noRollbackFor = IllegalArgumentException.class):即使抛出 IllegalArgumentException 也不回滚(适合某些参数校验失败但不影响主流程的场景)
- 可同时指定多个类型:rollbackFor = {SQLException.class, TimeoutException.class}
注意:rollbackFor 接收的是 Class extends Throwable>,不是实例;且优先级高于默认规则。
避免在事务方法内静默捕获并忽略异常
以下写法非常危险:
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
try {
accountDao.debit(fromId, amount);
accountDao.credit(toId, amount);
} catch (SQLException e) {
log.error("转账底层异常", e); // ❌ 仅记日志,没抛出,事务照常提交!
}
}
正确做法是:
- 不捕获,让异常向上冒泡(最简、最安全)
- 或捕获后包装为运行时异常再抛出:throw new ServiceException("转账失败", e)
- 或根据业务决定是否回滚:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()(手动标记回滚,适用于部分失败可容忍但当前分支必须撤回的复杂逻辑)
分层中异常的传递与封装
DAO 层抛 SQLException → Service 层不应直接暴露它。应转换为语义清晰的业务异常,并保留原始 cause:
try {
userDao.update(user);
} catch (SQLException e) {
throw new UserUpdateException("更新用户 " + user.getId() + " 失败", e);
}
这样既满足事务回滚(UserUpdateException 是 RuntimeException),又保障了错误溯源能力,API 调用方看到的是业务含义,运维查日志能顺藤摸到 JDBC 层问题。


















