Transaction rolled back because it has been marked as rollback-only


今天遇到了一个事务相关的异常 Transaction rolled back because it has been marked as rollback-only. 大致的异常堆栈是下面这样的。

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:871)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:708)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:631)
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
 at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
 at com.server.service.demo.impl.DemoManager$$EnhancerBySpringCGLIB$$1.test(<generated>)

通过搜索引擎,逐渐理解了问题产生的原因,特此记录。

首先,我们如果在项目中使用事务,只需要在对应的方法上写上@Transactional这个注解就好。如果一个方法就是一个单纯的事务还好,方法内部有异常,方法中所有的数据库操作立马回滚。

@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
  DemoChangeLog entity = new DemoChangeLog();
  entity.setId(2L);
  entity.setOperatorType("123456");
  this.save(entity);
  throw new IllegalArgumentException("123456");
}

如上,这个方法用@Transactional注解,在调用这个方法的时候先开启事务,然后一次执行对应的语句,直到save调用会执行sql语句,然后在最后一行抛出了异常,之前做的save操作导致数据库的变动立马回滚,这也是事务最常用的场景。

但是,如果一个事务调用另一个事务的情况下呢。如下

@Transactional(rollbackFor = Exception.class)
public void test() {
    demoChangeLogService.removeById(2);
    demoChangeLogService.test();
}

其中 demoChangeLogService.test() 如下,也用@Transactional注解

@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
    DemoChangeLog entity = new DemoChangeLog();
    entity.setId(2L);
    entity.setOperatorType("123456");
    this.save(entity);
    throw new IllegalArgumentException("123456");
}

这样就形成了一个@Transactional方法调用另一个@Transactional的情形。这也是常见的情形之一:此时程序执行 demoChangeLogService.removeById(2) 之后 执行 demoChangeLogService.test(),发现 demoChangeLogService.test()@Transactional 注解,于是看一下Spring的事务配置,默认情况下,如果已经开启了一个事务,然后又遇到一个需要开启事务的方法,此时会直接使用最开始开启的事务,也就是加入到原来的事务当中,而不是开始一个新的事务。那么接下来执行 demoChangeLogService.test() 内部方法的时候,在最后一行抛出了异常,这样就会导致当前事务回滚,那么 demoChangeLogService.removeById(2)demoChangeLogService.test() 中所有数据库操作都会回滚。

但是有的时候,我们在执行的时候期望 demoChangeLogService.removeById(2); 正常执行,如果demoChangeLogService.test();中执行异常的时候,只有demoChangeLogService.test();中的数据库操作回滚,而外层test() 方法中demoChangeLogService.removeById(2);的数据库操作不用回滚。此时我们可能会错误的写成

@Transactional(rollbackFor = Exception.class)
public void test() {
    demoChangeLogService.removeById(2);
    try{
       demoChangeLogService.test();
    }catch (Exception e){
      log.error("执行异常了",e)
    }
}

按照我们的理解, demoChangeLogService.test(); 内部异常了,那么内部的事务应该回滚,而外部方法中的异常被捕获了,理论上不应该被回滚啊。但是确实被回滚了,而且又抛出了一个异常 Transaction rolled back because it has been marked as rollback-only 这就是我今天遇到的异常。
原因是,demoChangeLogService.test();这个方法内部抛出了异常,对于当前事务来说应该执行回滚,但是由于我们把异常捕获了,导致外部的方法在执行完成后,事务应该是正常提交,此时就冲突了,一个要求回滚,一个要求正常提交,mysql蒙了,于是就抛出了上面这个异常。

解决方法:第二个方法的事务用开启一个新的事物而不继承原来的事务。

demoChangeLogService.test();

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test() {
    demoChangeLogService.removeById(2);
    try{
       demoChangeLogService.test();
    }catch (Exception e){
      log.error("执行异常了",e)
    }
}

可以达到上面的目的,但是用这种主机要万分小心,可能产生死锁。


评论