Spring事务:为什么catch了异常,事务还是回滚了?

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);
        }
    }
}

最佳实践建议

  1. 明确事务边界:在设计方法时,要明确哪些操作应该在同一个事务中,哪些应该分开。

  2. 合理使用传播级别

    • 默认使用REQUIRED
    • 对于非核心操作使用REQUIRES_NEW
    • 对于只读操作使用SUPPORTS或NOT_SUPPORTED
  3. 异常处理策略

    • 在事务方法内部谨慎使用try-catch
    • 如果catch了异常,考虑是否需要手动设置回滚
    • 使用声明式异常处理(@Transactional的rollbackFor/noRollbackFor属性)
  4. 测试验证:编写单元测试验证事务行为是否符合预期。

常见陷阱

  1. 自调用问题:在同一个类中调用@Transactional方法,事务注解会失效。

    java 复制代码
    @Service
    public class UserService {
        public void methodA() {
            this.methodB();  // 事务注解失效!
        }
        
        @Transactional
        public void methodB() {
            // ...
        }
    }
  2. 私有方法问题:@Transactional注解在私有方法上无效。

  3. 异常类型不匹配:默认只对RuntimeException和Error回滚,受检异常需要特别声明。

  4. 数据库引擎问题:MyISAM引擎不支持事务。

总结

Spring事务的这个问题看似奇怪,但理解了其背后的原理后就会明白,这是为了保证事务的一致性和原子性。核心要点总结如下:

  1. 事务状态标记是不可逆的:一旦事务被标记为rollback-only,就无法撤销。

  2. 异常处理的层次性:事务代理层的异常处理先于业务代码的catch块。

  3. 传播级别决定事务边界:不同的传播级别会创建不同的事务边界。

  4. 设计时要考虑事务影响范围:将可能失败的非核心操作放在独立事务中。

理解这些原理不仅能帮助我们避免踩坑,还能让我们设计出更健壮的事务处理逻辑。在实际开发中,我们应该根据业务需求合理选择事务传播级别,并在代码审查时特别注意事务相关代码的正确性。

希望这篇博客能帮助你彻底理解Spring事务的这个隐秘角落,让你在未来的开发中更加得心应手。

相关推荐
xuanloyer1 小时前
oracle从入门到精通--物理存储结构
数据库·oracle
chenzhou__1 小时前
LinuxC语言并发程序笔记补充
linux·c语言·数据库·笔记·学习·进程
别或许1 小时前
14、使用C++连接MySQL及接口
数据库·mysql
阿里云云原生1 小时前
阿里云 ARMS 自定义指标采集:打破传统 APM 局限,实现业务可视化监控
数据库·阿里云·云原生·oracle·arms
在坚持一下我可没意见1 小时前
Spring Boot 实战(一):拦截器 + 统一数据返回 + 统一异常处理,一站式搞定接口通用逻辑
java·服务器·spring boot·后端·spring·java-ee·tomcat
lu9up1 小时前
业务表异常阻塞导致接口超时处理案例
数据库·性能优化
San30.1 小时前
从 Mobile First 到 AI First:用 Python 和大模型让数据库“开口说话”
数据库·人工智能·python
古城小栈1 小时前
PostgreSQL 【vs】 MySQL
数据库·mysql·postgresql
安全系统学习2 小时前
网络安全漏洞之React 框架分析
数据库·安全·web安全·网络安全