一、问题描述
线上的日志中偶然排查到一个事务相关问题,头一次看到这个异常,一起来学习下。
1. 异常文案: Transaction rolled back because it has been marked as rollback-only
2. 定位问题: 经过初步分析,是事务嵌套引起(使用默认 REQUIRED 的传播级别),内部事务由于异常已经标记为rollback-only,但由于被try-catch,并未将异常抛出,所以后面的程序执行后又commit事务,抛出此异常。
所幸都可以正常回滚,不影响正常业务。
但我们的核心思想就是:不要让同一个事务标记为rollback-only后又commit。
3.问题复现:
创建类TestTransactionA,方法A()带事务注解:
/**
* @author Bober
* @date 2023/6/28 14:31
*/
@Component
public class TestTransactionA {
@Autowired
private TestTransactionB testTransactionB;
@Transactional
public void A() {
try {
testTransactionB.B();
} catch (Exception e) {
System.out.println("捕获异常");
}
}
}
创建类TestTransactionB,方法B()带事务注解:
/**
* @author Bober
* @date 2023/6/28 14:31
*/
@Component
public class TestTransactionB {
@Transactional
public String B (){
throw new RuntimeException();
}
}
运行后控制台结果:
二、原因分析:
当A方法的事物(REQUIRED),B方法的事物(REQUIRED),A调用B方法,在spring中,spring将会把这些事务合二为一。
当整个方法中每个子方法没有报错时,整个方法执行完才提交事务。
如果某个子方法有异常,spring将该事务标志为rollback only。但如果这个子方法没有将异常往上抛,或者主父方法将子方法抛出的异常捕获了,那么,该异常就不会触发事务进行回滚,事务就会在整个方法执行完后正常操作提交,这时就会造成Transaction rolled back because it has been marked as rollback-only的异常。(由于异常被标记了rollback only,但是又执行了commit,此时就会报这个错)。
三、解决方案
方法1:父方法不要捕获异常:
可以将A方法的try…catch去掉,让异常正常抛出,即可提前终止事务。
方法2:子方法的事务propagation属性换为NESTED:
@Component
public class TestTransactionB {
@Transactional(propagation = Propagation.NESTED)
public String B (){
throw new RuntimeException();
}
}
运行结果:
| 传播级别 | 功能描述 |
|---|---|
| Propagation.NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为和 Propagation.REQUIRED 效果一样。 |
方法3:手动回滚:
方法B( ) 被方法A( ) try-catch后,手动回滚异常,并返回异常码。外层判断调用方法结果,是否再手动回滚。虽然感知不到异常,但是通过判断调用方返回结果,是否手动回滚。不会让事务commit。
/**
* @author Bober
* @date 2023/6/28 14:31
*/
@Component
public class TestTransactionA {
@Autowired
private TestTransactionB testTransactionB;
@Transactional
public void A() {
try {
testTransactionB.B();
} catch (Exception e) {
//设置手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//此时后置逻辑可以执行到
System.out.println("捕获异常");
}
}
}
By the way:
Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:
-
Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
-
Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
-
Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
-
Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
-
Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
-
Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
-
Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。