Spring事务:为什么catch了异常,事务还是回滚了?
前言
在日常开发中,Spring的事务管理是我们最常用的功能之一。但你是否遇到过这样的场景:在事务方法中调用了另一个方法,明明用try-catch捕获了异常,为什么事务还是回滚了?更奇怪的是,当被调用的方法有 @Transactional注解时会出现问题,没有注解时却能正常运行?
今天,我们就来深入探讨这个看似诡异的现象,揭开Spring事务传播机制的神秘面纱。
一个令人困惑的例子
让我们从一个实际的代码案例开始:
java
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public Integer updateUser(UserUpdateParam param) {
// 更新用户信息的业务逻辑
userService.updateUser(userPo);
try {
userService.test(); // 调用另一个方法
} catch (Exception e) {
log.error("数据异常"); // 捕获异常,不继续抛出
}
return 0;
}
}
// 在另一个Service中
@Service
public class OtherService {
// 情况1:没有@Transactional注解
public void test() {
throw new ErrorCodeException("测试异常");
}
// 情况2:有@Transactional注解
@Transactional(rollbackFor = Exception.class)
public void test() {
throw new ErrorCodeException("测试异常");
}
}
当test()方法没有@Transactional注解时,updateUser()方法正常执行,事务成功提交。
但当test()方法加上@Transactional注解后,updateUser()方法会抛出:
org.springframework.transaction.UnexpectedRollbackException:
Transaction rolled back because it has been marked as rollback-only
为什么会这样?让我们一步步深入。
事务传播机制的核心原理
1. Spring事务的本质:AOP代理
首先要理解,Spring的事务管理是基于AOP实现的。当我们给方法加上@Transactional注解时,Spring会为这个类创建一个代理对象。实际调用流程是这样的:
java
// 简化版的代理实现
public class TransactionProxy {
public Object invoke(Method method, Object[] args) {
// 1. 开启事务
TransactionStatus status = beginTransaction();
try {
// 2. 调用原始方法
Object result = target.method(args);
// 3. 提交事务
commit(status);
return result;
} catch (Exception ex) {
// 4. 根据异常类型决定回滚还是提交
if (shouldRollback(ex)) {
rollback(status);
}
throw ex;
}
}
}
2. 事务传播的7种行为
Spring定义了7种事务传播行为,最常用的是:
- REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
- NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务
3. 关键对象:TransactionStatus
每个事务都有一个TransactionStatus对象,其中有一个非常重要的标志:
java
class DefaultTransactionStatus {
private boolean rollbackOnly = false; // 回滚标记
public void setRollbackOnly() {
this.rollbackOnly = true; // 一旦设置为true,事务就注定要回滚
}
}
这个标记一旦被设置,就无法取消。这是理解整个问题的关键。
为什么会有不同的表现?
场景分析:无@Transactional注解
当test()方法没有@Transactional注解时:
执行流程:
1. updateUser()开始 → 开启事务T1
2. 调用test() → 没有事务代理,直接执行test()方法
3. test()抛出异常 → 异常直接传播到updateUser()的catch块
4. updateUser()捕获异常 → 异常在业务层被消化
5. updateUser()正常结束 → 事务代理提交事务(成功)
关键点:异常在到达事务代理层之前就被捕获了,事务管理器根本不知道有异常发生。
场景分析:有@Transactional注解
当test()方法有@Transactional注解时:
执行流程:
1. updateUser()开始 → 开启事务T1
2. 调用test() → Spring发现test()有@Transactional注解
3. 创建事务代理 → 由于默认传播级别是REQUIRED,test()会加入updateUser()的事务
4. test()抛出异常 → 异常首先被test()的事务代理捕获
5. test()的事务代理:检查异常类型 → 符合回滚条件 → 标记当前事务T1为rollback-only
6. 异常继续传播到updateUser()的catch块
7. updateUser()捕获异常 → 但事务已在步骤5被标记为rollback-only
8. updateUser()正常结束 → 事务管理器提交时发现rollback-only → 强制回滚并抛出UnexpectedRollbackException
关键点:异常先被内部方法的事务代理处理,标记了事务状态,然后才传播到外部方法的catch块。
可视化对比
让我们用更直观的方式来看这两种情况的区别:
情况2: test()有事务注解 情况1: test()无事务注解 调用test方法 updateUser事务代理开始 创建test事务代理 test方法执行 抛出异常 test事务代理捕获异常 标记事务为rollback-only 异常传播到updateUser的catch catch块处理异常 事务代理提交时发现rollback-only 强制回滚并抛出UnexpectedRollbackException 调用test方法 updateUser事务代理开始 test方法执行 抛出异常 异常传播到updateUser的catch catch块处理异常 事务代理提交成功
源码层面的证据
让我们看看Spring是如何处理这个过程的:
java
// TransactionAspectSupport.invokeWithinTransaction() 简化版
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
TransactionAttribute txAttr = getTransactionAttribute(method);
final TransactionStatus status = determineTransactionStatus(txAttr);
try {
// 执行目标方法
Object result = invocation.proceedWithInvocation();
// 提交事务
commitTransactionAfterReturning(status);
return result;
} catch (Throwable ex) {
// 异常处理:决定是否回滚
completeTransactionAfterThrowing(txAttr, status, ex);
throw ex;
}
}
// 关键方法:异常后完成事务
protected void completeTransactionAfterThrowing(TransactionAttribute txAttr,
TransactionStatus status, Throwable ex) {
// 判断是否需要回滚
if (txAttr != null && txAttr.rollbackOn(ex)) {
try {
// 标记事务为回滚
status.setRollbackOnly();
} catch (RuntimeException re) {
throw re;
}
} else {
// 否则提交
commitTransactionAfterReturning(status);
}
}
从源码可以看出,当有@Transactional注解的方法抛出异常时,会先调用completeTransactionAfterThrowing()方法,在这个方法中会调用status.setRollbackOnly(),这个标记一旦设置就无法撤销。
实际验证
我们可以通过代码验证这个现象:
java
@Transactional(rollbackFor = Exception.class)
public Integer updateUser(UserUpdateParam param) {
try {
System.out.println("调用test前,事务是否标记回滚: " +
TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
userService.test();
} catch (Exception e) {
System.out.println("catch块中,事务是否标记回滚: " +
TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
log.error("数据异常");
}
System.out.println("方法结束前,事务是否标记回滚: " +
TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
return 0;
}
输出结果:
- 当test()无事务注解:false, false, false
- 当test()有事务注解:false, true , true
解决方案
了解了问题的原因,我们来看看如何解决。
方案1:使用REQUIRES_NEW传播级别
java
@Service
public class OtherService {
@Transactional(propagation = Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
public void test() {
throw new ErrorCodeException("测试异常");
}
}
这样test()会在独立的事务中执行,它的回滚不会影响外部事务。
方案2:使用NOT_SUPPORTED传播级别
java
@Service
public class OtherService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void test() {
throw new ErrorCodeException("测试异常");
}
}
这样test()会在非事务环境下执行,不会影响外部事务。
方案3:调整业务逻辑
有时候,我们需要重新考虑业务逻辑的设计:
java
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public Integer updateUser(UserUpdateParam param) {
// 主要业务逻辑
try {
userService.test();
} catch (Exception e) {
log.error("test方法执行失败,但不影响主流程", e);
// 可以在这里进行补偿操作
compensateOperation();
}
return 0;
}
// 使用REQUIRES_NEW确保独立事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test() {
// 非核心业务逻辑
throw new ErrorCodeException("测试异常");
}
}
方案4:手动控制事务边界
java
@Service
public class UserService {
@Autowired
private PlatformTransactionManager transactionManager;
public Integer updateUser(UserUpdateParam param) {
// 手动管理事务
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 主要业务逻辑
// 调用test方法,但在独立事务中
testInNewTransaction();
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
return 0;
}
private void testInNewTransaction() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// test方法的逻辑
throw new ErrorCodeException("测试异常");
} catch (Exception e) {
transactionManager.rollback(status);
// 这里可以记录日志,但不传播异常
log.error("test方法失败", e);
}
}
}
最佳实践建议
-
明确事务边界:在设计方法时,要明确哪些操作应该在同一个事务中,哪些应该分开。
-
合理使用传播级别:
- 默认使用REQUIRED
- 对于非核心操作使用REQUIRES_NEW
- 对于只读操作使用SUPPORTS或NOT_SUPPORTED
-
异常处理策略:
- 在事务方法内部谨慎使用try-catch
- 如果catch了异常,考虑是否需要手动设置回滚
- 使用声明式异常处理(@Transactional的rollbackFor/noRollbackFor属性)
-
测试验证:编写单元测试验证事务行为是否符合预期。
常见陷阱
-
自调用问题:在同一个类中调用@Transactional方法,事务注解会失效。
java@Service public class UserService { public void methodA() { this.methodB(); // 事务注解失效! } @Transactional public void methodB() { // ... } } -
私有方法问题:@Transactional注解在私有方法上无效。
-
异常类型不匹配:默认只对RuntimeException和Error回滚,受检异常需要特别声明。
-
数据库引擎问题:MyISAM引擎不支持事务。
总结
Spring事务的这个问题看似奇怪,但理解了其背后的原理后就会明白,这是为了保证事务的一致性和原子性。核心要点总结如下:
-
事务状态标记是不可逆的:一旦事务被标记为rollback-only,就无法撤销。
-
异常处理的层次性:事务代理层的异常处理先于业务代码的catch块。
-
传播级别决定事务边界:不同的传播级别会创建不同的事务边界。
-
设计时要考虑事务影响范围:将可能失败的非核心操作放在独立事务中。
理解这些原理不仅能帮助我们避免踩坑,还能让我们设计出更健壮的事务处理逻辑。在实际开发中,我们应该根据业务需求合理选择事务传播级别,并在代码审查时特别注意事务相关代码的正确性。
希望这篇博客能帮助你彻底理解Spring事务的这个隐秘角落,让你在未来的开发中更加得心应手。