Spring事务:异常不回滚、marked as rollback-only无法提交

bear-blog / 2024-09-13 / 原文

1、非代理对象调用事务方法时事务不生效

A实现了接口IA,类A的方法a()上使用@Transaction声明了事务,想要在调用该方法时,使a()的事务生效,就必须使用类A的Aop代理对象来调用。

1)正常情况下,我们在类B中注入A的Bean对象,这个对象就是代理对象,使用该对象调用a()方法时,a()的事务是正常生效的。

2)另一个常见情况就是类A中的方法a1()中直接调用事务方法a(),无论是a()还是this.a(),此时调用方法a()的不是代理对象,而是类A的原生对象,这种情况下,a()的事务是不生效的,也就是a()中发生异常事务不会回滚。如果想要在这个情况下让a()的事务生效,我们需要拿到类A的Aop代理对象(或者拿到本类的Bean对象,这就是代理对象)。首先在配置类中使用@EnableAspectJAutoProxy(exposeProxy = true)暴露代理对象,再在代码中使用IA iA = (IA)AopContext.currentProxy();获取到类A的代理对象iA,再使用iA调用方法a(),此时a()的事务就是生效的。

@Service
public class A implements IA {

  @Override
  @Transactional(rollback = Exception.class)
  public void a(){
    // 代码块
  }
  
  @Override
  @Transactional
  public void a1(){
    // 原生对象调用
    a(); // 方法a()事务不生效

    // 代理对象调用
    IA iA = (IA)AopContext.currentProxy();
    iA.a(); // 方法a()事务生效
  }
}

@Component
public class B {
  
  @Autowired
  private IA iA;
  
  public void b() {
    iA.a(); // 方法a()事务生效
  }
}

/**
 * 启动类(配置类)
 */
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true) // proxyTargetClass:ture cglib代理,false jdk动态代理
@EnableAspectJAutoProxy(exposeProxy = true) // 开启暴露代理对象
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication .class,args);
        System.out.println("(♥◠‿◠)ノ゙  启动成功   ლ(´ڡ`ლ)゙ ");
    }
}

2、异常Transaction rolled back because it has been marked as rollback-only的触发原因和解决方式

这个异常往往是在代码执行完成最后提交事务时抛出的,所以我们无法直接定位异常的源头。

在事务方法中,遇到异常便会触发回滚,如果我们不想回滚,就会使用try catch包裹住可能抛出异常的代码块。

但有一种情况,就算在代码中捕获了所有异常,但执行到最后还是会碰到标题所示的异常,导致事务提交失败。这是因为我们在事务生效的方法a()中调用事务生效的方法b(),并且有时为了不让b()的异常影响a()的提交,我们会在a()中用try catch包裹住了b()的调用。但实际上,如果b()中发生了异常,异常被捕获,后续代码正常执行,但最后还是会碰到标题的异常。这是因为a()b()处在同一事务中,如果在b()或者b()中调用的与其处在同一事务中的下级方法中出现了异常但没有被抛给事务管理器时,这个事务就会被标记为rollback-only,到最后事务提交时,才会提示异常。

如果想要解决这个问题,并且依旧不想让b()的异常抛出,我们只需要让a()b()不处在同一事务中就可以了,@Transaction的propagation属性默认值是REQUIRED,意思是当前有事务则加入,没有则创建一个事务,就是这个配置让a()b()处在同一事务中,所以,我们只要把b()的事务改成@Transactional(propagation = Propagation.NESTED),使b()创建一个嵌套事务并加入,此时b()的异常就不会导致a()回滚,并且不会遇到标题的异常。

@Service
public class A implements IA {

  @Autowired
  private IB iB;

  @Override
  @Transactional(rollback = Exception)
  public void a(){
    try{
      iB.b();
    } catch(Exception e) {
      // ...
    }
  }
}

@Service
public class B implements IB {
  
  @Override
  @Transaction(rollback = Exception)
  // @Transaction(rollback = Exception, propagation = Propagation.NESTED) // 解决方法1,嵌套事务
  // @Transaction(rollback = Exception, propagation = Propagation.REQUIRES_NEW) // 解决方法2,全新事务
  // @Transaction(rollback = Exception, propagation = Propagation.NOT_SUPPORTED) // 解决方法3,关闭事务
  public void b() {
    throw new RuntimeException();
  }

  /**
   * 本类方法中调用时注意,一般情况的调用方式不会触发rollback-only异常
   */
  @Override
  @Transactional(rollback = Exception)
  public void b1(){
    // 原生对象调用
    try{
      b(); // 方法b()事务不生效,不会触发rollback-only异常
    } catch(Exception e) {
      // ...
    }

    // 代理对象调用
    IB iB = (IB)AopContext.currentProxy();
    try{
      iB.b(); // 方法b()事务生效,会触发rollback-only异常
    } catch(Exception e) {
      // ...
    }
  }
}