声明式事务在哪些情况下会失效?

5 28~36 min


三分恶面渣逆袭:声明式事务的几种失效的情况

1、@Transactional 应用在非 public 修饰的方法上

如果 Transactional 注解应用在非 public 修饰的方法上,Transactional 将会失效。

是因为在 Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute方法,获取 Transactional 注解的事务配置信息。

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
    }
}

此方法会检查目标方法的修饰符是否为 public,不是 public 则不会获取 @Transactional 的属性配置信息。

2、@Transactional 注解属性 propagation 设置错误

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行;错误使用场景:在业务逻辑必须运行在事务环境下以确保数据一致性的情况下使用 SUPPORTS。

  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:总是以非事务方式执行,如果当前存在事务,则挂起该事务。错误使用场景:在需要事务支持的操作中使用 NOT_SUPPORTED。

  • TransactionDefinition.PROPAGATION_NEVER:总是以非事务方式执行,如果当前存在事务,则抛出异常。错误使用场景:在应该在事务环境下执行的操作中使用 NEVER。

3、@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 用来指定能够触发事务回滚的异常类型。Spring 默认抛出未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。

三分恶面渣逆袭:Spring默认支持的异常回滚

// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)

若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。

4、同一个类中方法调用,导致@Transactional 失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类 Test,它的一个方法 A,A 调用本类的方法 B(不论方法 B 是用 public 还是 private 修饰),但方法 A 没有声明注解事务,而 B 方法有。

则外部调用方法 A 之后,方法 B 的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况呢?其实还是由 Spring AOP 代理造成的,因为只有事务方法被当前类以外的代码调用时,才会由 Spring 生成的代理对象来管理。

 //@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
    CityInfoDict cityInfoDict = new CityInfoDict();
    cityInfoDict.setCityName("2");
    /**
     * B 插入字段为 3的数据
     */
    this.insertB();
    /**
     * A 插入字段为 2的数据
     */
    int insert = cityInfoDictMapper.insert(cityInfoDict);
    return insert;
}

@Transactional()
public Integer insertB() throws Exception {
    CityInfoDict cityInfoDict = new CityInfoDict();
    cityInfoDict.setCityName("3");
    cityInfoDict.setParentCityId(3);

    return cityInfoDictMapper.insert(cityInfoDict);
}

这种情况是最常见的一种@Transactional 注解失效场景。

@Transactional
private Integer A() throws Exception {
    int insert = 0;
    try {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName("2");
        cityInfoDict.setParentCityId(2);
        /**
         * A 插入字段为 2的数据
         */
        insert = cityInfoDictMapper.insert(cityInfoDict);
        /**
         * B 插入字段为 3的数据
        */
        b.insertB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

如果 B 方法内部抛了异常,而 A 方法此时 try catch 了 B 方法的异常,那这个事务就不能正常回滚了,会抛出异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

@Transactional
@GetMapping("/test")
public Integer A() throws Exception {
    CityInfoDict cityInfoDict = new CityInfoDict();
    cityInfoDict.setCityName("2");
    /**
     * B 插入字段为 3的数据
     */
    this.insertB();
    /**
     * A 插入字段为 2的数据
     */
    int insert = cityInfoDictMapper.insert(cityInfoDict);
    return insert;
}

@Transactional()
public Integer insertB() throws Exception {
    CityInfoDict cityInfoDict = new CityInfoDict();
    cityInfoDict.setCityName("3");
    cityInfoDict.setParentCityId(3);

    return cityInfoDictMapper.insert(cityInfoDict);
}

在修改后的代码中(方法 A 改为 public 且保留 @TransactionalinsertB 仍通过 this.insertB() 自调用),事务的行为如下


1. 事务是否生效?

  • 方法 A 的事务会生效:因为它是 public 且通过代理对象调用(例如通过 HTTP 请求触发 @GetMapping)。

  • 方法 insertB 的事务不会生效:虽然它是 public 且带有 @Transactional,但由于自调用(this.insertB()),Spring 的代理机制被绕过,导致事务注解失效。


2. 实际事务行为

  • 所有数据库操作(insertBA 中的插入)会在 A 的事务中执行

    1. 外部调用 A() 时,Spring 代理会拦截并开启事务。

    2. A() 内部通过 this.insertB() 调用 insertB() 时,由于是自调用,insertB()@Transactional 会被忽略。

    3. insertB() 的插入操作会直接加入 A() 的事务中。

    4. 最终,所有操作在 A() 的同一事务中提交或回滚。


3. 关键问题:自调用导致的事务失效

  • 为什么 insertB 的事务不生效?
    Spring 的事务基于 AOP 代理,只有通过代理对象调用的方法才会触发事务逻辑。自调用(this.insertB())直接调用目标对象的方法,绕过了代理,因此 insertB@Transactional 失效。

  • 传播行为的配置会生效吗?
    如果 insertB 设置了传播行为(例如 @Transactional(propagation = Propagation.REQUIRES_NEW)),该配置也会失效,因为自调用时 Spring 不处理事务注解。


4. 示例验证

假设 insertB 中抛出异常:

@Transactional
@GetMapping("/test")
public Integer A() throws Exception {
    this.insertB(); // insertB 抛出异常
    cityInfoDictMapper.insert(...); // 不会执行
    return 1;
}

@Transactional
public Integer insertB() throws Exception {
    cityInfoDictMapper.insert(...); // 插入操作
    throw new Exception("回滚测试"); // 抛出异常
}
  • 结果:由于 A() 的事务生效,insertB 的异常会导致 A() 的事务回滚,所有操作撤销。

  • 原因insertB 的异常通过自调用传播到 A() 的事务中,触发全局回滚。


5. 如何让 insertB 的事务生效?

需解决自调用问题:

  1. insertB 移到其他 Bean 中,通过依赖注入调用:

    @Autowired
    private OtherService otherService;
    
    @Transactional
    public Integer A() {
        otherService.insertB(); // 通过代理调用
        // ...
    }
  2. 通过代理对象调用自身方法(需启用 exposeProxy):

    @Transactional
    public Integer A() {
        ((YourClass) AopContext.currentProxy()).insertB(); // 通过代理调用
        // ...
    }

    (需在启动类添加 @EnableAspectJAutoProxy(exposeProxy = true)


最终答案

  • 方法 A 的事务会生效,所有操作(包括 insertB)在 A 的事务中执行。

  • insertB@Transactional 不生效(自调用绕过代理),其传播行为、隔离级别等配置均无效。

  • 若需要 insertB 独立事务,必须通过代理调用或拆分到其他 Bean 中。

在同一个类中,非事务方法(未标注 @Transactional 的方法)直接调用事务方法(标注 @Transactional 的方法)时,事务不会生效。这是因为 Spring 的事务管理基于 AOP 代理机制,而自调用(通过 this 调用)会绕过代理,导致事务注解失效。

总结

  • 自调用事务方法不生效:非事务方法直接调用同类中的事务方法时,事务失效。

  • 根本原因:绕过代理,未触发 Spring 事务逻辑。

  • 解决方法

    1. 将事务方法拆分到其他 Bean。

    2. 通过 AopContext.currentProxy() 获取代理对象调用。

    3. 通过接口注入自身代理。

事务生效的核心原则:确保通过 Spring 代理对象调用事务方法