全面解析Spring事务的失效以及如何避免

一.什么时候会发生事务失效?

Spring的@Transactional注解是实现声明式事务管理的强大工具,但在某些情况下,开发者可能会遇到事务注解失效的问题,导致预期中的事务管理机制不起作用。理解为什么会发生事务注解失效是避免这一问题的关键。以下是事务注解失效的一些常见原因:

1.方法访问级别不当

Spring的事务管理默认要求事务方法必须是public。如果你将一个使用@Transactional注解的方法设置为private、protected或是package-private,事务代理将无法正常工作,导致事务注解失效。

2.事务方法在同一个类中调用

Spring事务管理是基于代理的。当一个事务方法直接从同一个类的另一个方法内部调用时,由于代理是基于方法调用的外部拦截,这种"自调用"情况会导致事务失效。

@Service
public class MyService {

    public void methodA() {
        // 调用本类的事务方法
        methodB();
    }

    @Transactional
    public void methodB() {
        // 执行一些数据库操作
    }
}

这种情况下,methodB() 是通过 普通的 Java 方法调用 执行的,而不是通过代理对象调用。也就是说,Spring 的事务拦截器(TransactionInterceptor)无法拦截到这次调用,所以 @Transactional 注解就不会生效,事务就不会被真正开启。

3.异常处理不当

@Transactional注解默认只对运行时异常(RuntimeException及其子类)进行回滚。如果方法内部抛出的是检查型异常(Exception的直接子类),而不是运行时异常,且没有通过@TransactionalrollbackFor属性明确指定异常类型,事务将不会回滚,导致事务失效。

❌ 默认行为下,不回滚

@Transactional
public void doSomething() throws Exception {
    // 执行一些数据库操作
    if (true) {
        throw new Exception("受检异常"); // 不会触发事务回滚
    }
}

 ✅ 显式指定回滚异常类型

@Transactional(rollbackFor = Exception.class)
public void doSomething() throws Exception {
    // 执行一些数据库操作
    if (true) {
        throw new Exception("受检异常"); // 事务将回滚
    }
}

4. 事务管理器配置错误

在Spring配置中,如果未正确配置事务管理器,或者在多事务管理器的情况下未指定正确的事务管理器,也可能导致@Transactional注解失效。

5.数据源或持久化框架配置不正确

正确配置数据源和持久化框架(如Hibernate或JPA)对于事务管理至关重要。如果数据源未配置为支持事务的数据源,或者持久化框架的配置不支持当前的事务管理方式,都可能导致事务失效。

6. Spring Bean的错误创建或注入

如果使用@Transactional注解的类没有被Spring容器管理,即该类的实例不是通过Spring创建的Bean,而是通过new关键字直接实例化的,那么@Transactional注解将不会生效,因为Spring无法对这样的实例应用代理和事务管理。

public class OrderService {

    @Transactional
    public void createOrder() {
        // 1. 保存订单
        // 2. 抛出异常
    }
}
public class Test {
    public static void main(String[] args) {
        OrderService service = new OrderService(); // 手动new出来
        service.createOrder(); // @Transactional 无效,不会开启事务
    }
}

7. 事务传播行为配置不当

事务的传播行为指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。Spring提供了多种的事务传播行为。

  1. REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是常规的传播行为,也是默认的,适用大多数情况。

  2. REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于需要独立事务执行的场景,不受外部事务影响。

  3. SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。适用于不需要强制事务的场景,可以在方法执行期间暂停禁用事务。

  4. NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,可以在方法执行期间暂停禁用事务。

  5. MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。适用于必须在事务支持的场景,如果没有事务则会抛出异常。

  6. NESTED:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。嵌套事务是外部事务的一部分,可以独立提交或者回滚。适用于需要在嵌套事务中执行的场景。

  7. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则会抛出异常。

通过 @Transactional 注解的 propagation 属性来指定事务传播行为。例如,如果一个事务方法被另一个已经在运行中的事务方法调用,并且事务传播行为设置为REQUIRES_NEW,那么原有事务将被挂起,新的事务开始执行。如果对这些行为的理解不正确,可能会导致事务管理复杂化,甚至失效。

二.案例分析

1.案例1

@Transactional
public int insert(Test test) {
    testMapper.insert(test);
    insert2(test);
    return 1;
}

@Transactional(propagation = Propagation.NEVER)
public int insert2(Test test){
    return testMapper.insert(test);
}

Propagation.NEVER的作用是不在事务中执行,如果之前已经存在事务的话,那么直接抛出异常不再执行。上述代码中,我们期望的结果是执行到insert2方法时会抛出已经存在事务的错误,然而实际上是什么错误也没有抛出,这两个insert操作都成功执行了。

问题本质:自调用导致事务失效

在 insert() 中调用 insert2(),这是 “类内部方法之间的调用”(自调用)。这是事务不生效的核心原因。

分析:

其实事务也利用了AOP原理,Spring通过代理对象来实现事务管理。当你标注了 @Transactional注解 ,Spring会给你的类创建一个代理类(默认是JDK动态代理或者CGLIB代理)。这个代理类包裹了真实的业务方法,他在调用方法前会先判断是否要开启事务,在方法后提交或回滚事务。

  1. 方法执行时,其实是执行了事务代理对象的方法

  2. 事务代理对象的方法中首先会开启事务,获取数据源连接

  3. 然后再执行代理对象中的target也就是普通对象的方法,这里就是执行真正我们的业务

  4. 然后事务代理对象会判断上述执行过程中有没有出现异常,进而判断是 commit 还是 rollback

要让 Propagation.NEVER 生效,你必须通过代理对象调用 insert2() 方法。有两种解决方式:

  1. 将调用交由 Spring 自己的代理对象执行

  2. 把 insert2 拆到另一个类中

2.案例2

@RequestMapping("/insert/{id}")
public Integer insert(@PathVariable Long id){
    Test test = new Test();
    test.setId(id);
    test.setColumn1("test1-" + id);
    test.setColumn2("test2-" + id);
    test.setColumn3("test3-" + id);
    test.setColumn4("test4-" + id);
    test.setColumn5("test5-" + id);
    test.setColumn6("test6-" + id);
    test.setNumber(id);
    int result = 0;
    try {
        result = testService.insert(test);
    }catch (Exception e) {
        log.error("出现异常",e);
    }
    return result;
}
public int insert(Test test) {
    testMapper.insert(test);
    int i = 1 / 0;
    return 1;
}

大部分人会觉得service的方法已经被try住了,肯定不会被回滚了,然后事实并非如此,在这个案例中依旧能够被回滚。

执行流程:

  • 从开启事务获得数据库的connect -> 执行添加操作 -> 根据是否出现异常进行提交或回滚这整个逻辑都在代理对象中的invokeWithinTransaction方法内的

  • try包裹的是service的方法,也就是说try包裹的范围是在invokeWithinTransaction方法的外面,异常依旧能被感知到还是能回滚的

修改后的版本:

@Transactional
public int insert(Test test) {
    testMapper.insert(test);
    try {
        int i = 1 / 0;
    } catch (Exception e) {
        // 吞掉异常
        // 什么也不做
    }
    return 1;
}

 这时候就不会回滚,因为异常被吞了,Spring就感知不到任何异常发生,默认会进行 commit。

三.总结

Spring 的事务机制基于 AOP 代理实现,虽然使用上非常简洁,但也隐藏了许多容易被忽略的陷阱。事务失效的根本原因大多源于对 Spring 代理机制的不了解使用方式不当。本文通过多种典型场景详细剖析了事务失效的常见原因,包括方法访问级别、自调用、异常类型、配置错误等问题,并结合具体案例阐明了事务的执行流程与传播行为。

开发者在使用 @Transactional 时应特别注意:

  • 必须通过 Spring容器管理的代理对象 调用事务方法;

  • 事务方法应为 public

  • 要避免在事务方法中手动捕获并吞掉异常;

  • 在需要对非运行时异常回滚时,使用 rollbackFor 明确指定;

  • 熟悉事务传播行为,合理设计业务调用关系。

只有深入理解 Spring 事务的原理与限制,才能在实际开发中写出健壮、可控的事务逻辑,避免潜在的数据一致性风险。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

他们都叫我0xCAFEBABE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值