今天遇到了一个事务相关的异常 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)
}
}
可以达到上面的目的,但是用这种主机要万分小心,可能产生死锁。