说透事务异常UnexpectedRollbackException: Transaction rolled back because it has been m
说透事务异常UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
问题复现
环境:SpringBoot2.7.15
说明:方便起见,Service层不再写接口+实现类的方式,直接采用实现类。
首先创建两个Service
package com.sjx.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class AService {
@Resource
private BService bService;
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public void doSomeThing() {
try {
bService.hello();
} catch (Exception e) {
//e.printStackTrace();
}
}
}
package com.sjx.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BService {
@Transactional // 报错
//@Transactional(propagation = Propagation.REQUIRED) // 报错
//@Transactional(propagation = Propagation.NESTED) // 不报错,外层事务正常提交
//@Transactional(propagation = Propagation.REQUIRES_NEW) // 不报错,外层事务正常提交
//@Transactional(noRollbackFor = RuntimeException.class) // 不报错,外层事务正常提交
//nothing //不报错 什么都不加
public void hello() {
throw new RuntimeException("helloException");
}
}
然后启动时调用AService的doSomeThing方法
@SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Resource
private AService aService;
@Override
public void run(String... args) throws Exception {
System.out.println("开始执行业务方法");
try {
aService.doSomeThing();
} catch (Exception e) {
e.printStackTrace();
}
}
}
控制台输出如下
开始执行业务方法
2023-10-17 23:15:44.588 INFO 7204 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-10-17 23:15:44.767 INFO 7204 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
at com.sjx.service.AService$$EnhancerBySpringCGLIB$$df5f6d73.doSomeThing(<generated>)
at com.sjx.Application.run(Application.java:30)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:752)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
at com.sjx.Application.main(Application.java:17)
总结
出现UnexpectedRollbackException
的必要条件是:事务方法嵌套,位于同一个事务中,方法位于不同的文件;子方法抛出异常,被上层方法捕获和消化。
解决方案
- 外层事务catch语句中增加
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public void doSomeThing() {
try {
bService.hello();
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//e.printStackTrace();
}
}
bService.hello();
作为内层事务抛出异常后会把内层事务的rollback-only状态改为true,外层事务的rollback-only状态还是false,所以尝试提交外层事务就会抛出UnexpectedRollbackException
异常。所以加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
之后,手动标记外层事务rollback-only状态为ture,这样外层事务就不会抛出异常了,不过外层事务就会被回滚,不会生效。
- 修改内层事务的传播级别为
Propagation.NESTED
或Propagation.REQUIRES_NEW
- NESTED:把内层事务标记为嵌套事务,嵌套事务发生异常回滚后,外层事务只会回滚到savepoint。也就是回滚部分事务
- REQUIRES_NEW:把内层事务标记为一个新事务。内层事务异常,外层事务不会混滚
思考
既然UnexpectedRollbackException
的异常是由于内层事务和外层事务的rollback-only状态不一致导致的。那我们在写业务代码的时候。
方法Method中执行A、B、C……;应该考虑实际业务情况ABC发生异常时是否需要回滚而来设置传播级别。
如果传播级别都是Propagation.REQUIRED
,那内层事务的异常就不允许被吞掉!应该抛出到上层由上层积极处理。要么跑出去,要么手动回滚事务。总之不允许外层事务吞异常。否则就会出现烦人的Transaction rolled back because it has been marked as rollback-only
原因
异常发生的位置:org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback
探究
先来看一下Spring是如何处理事务的
图片里的四个框框暂且按顺序标记为①②③④吧
拿上例来说,AService#doSomeThing
方法实际调用是在②处
retVal = invocation.proceedWithInvocation();
又由于AService#doSomeThing
调用了Bservice#hello
方法,所以会递归的又走②
retVal = invocation.proceedWithInvocation();
第二次进入这行方法就是执行Bservice#hello
,但是Bservice#hello
中抛出了异常,于是会被捕捉到,走进第③处代码对Bservice#hello
方法进行回滚。我们来看一下回滚逻辑
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
// 如果事务信息不是null
if (txInfo != null && txInfo.getTransactionStatus() != null) {
// 判断是否回滚指定的异常。也就是对rollbackFor属性中的字段进行判断
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 执行回归的代码
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 走到该分支说明此时发生的的异常不需要回滚
else {
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}
总结这段回滚代码:Spring默认只回滚RuntimeException
和Error
,判断逻辑如下org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
所以一般我们都要设置注解中的rollbackFor
属性为Exception.class
Spirng在上述代码中的回滚逻辑卸载了txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
中,接下来来看一下这个rollback
方法
public final void rollback(TransactionStatus status) throws TransactionException {
// 如果事务已经完成,则抛出异常
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
// 处理预期的异常,第二个参数false表示预期,true表示不是预期的异常
processRollback(defStatus, false);
}
接下来继续看processRollback
这个方法。这个方法的第二个参数就是引起整篇文章讨论的这个错误的原因!第二个参数为true,则就会抛出该异常。
Transaction rolled back because it has been marked as rollback-only
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
// 触发一些回滚前的操作
triggerBeforeCompletion(status);
// 是否有保存点
if (status.hasSavepoint()) {
// 实际使用Connection数据库连接的rollback方法回滚事务
status.rollbackToHeldSavepoint();
}
// 是否是新事务,Requires_New会走到这里,最外层的Required的方法也会走到这里。
else if (status.isNewTransaction()) {
// 实际使用Connection数据库连接的rollback方法回滚事务
doRollback(status);
}
// 如果两个事务都是默认的传播级别,就会走到这里,也就是在这个else中报的异常
else {
// 有异常
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
// 事务回滚标记的代码 ,这里只做标记,并不是真正的回滚
doSetRollbackOnly(status);
}
}
// ...
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
// 比如TransactionEventListener注解的逻辑就是借助这行代码实现的
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
// 敲黑板了!!!就是在这里报的错。但是必须要该方法的第二个参数为true
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
// 更新complete为true,表示完成了
cleanupAfterCompletion(status);
}
}
接下来就来看一下事务回滚的代码doSetRollbackOnly
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject)status.getTransaction();
txObject.setRollbackOnly();
}
通过断点可以看到,最终就是为了把一个connection持有对象的readOnly属性置为true
但是知识设置了一下readOnly属性置为true,并没有真正的回滚事务。真正回归事务还在commit方法的逻辑里
最终执行完之后,整个Bservice#hello
方法也就执行完了。由于AService#doSomeThing
中吞掉了异常,所以在AService#doSomeThing
方法执行完毕后不会进入③处的代码。自然就顺理成章的走到了④处的代码。也就是提交事务。我们来看一下这个方法
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
如法炮制,提交逻辑是在commit
方法中
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
// 如果使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();手动
// 设置TransactionStatus的readOnly为ture,就走这块逻辑,这就是上面的解决方案。processRollback第二个参数为false
// 就不会抛出UnexpectedRollbackException异常了
if (defStatus.isLocalRollbackOnly()) {
processRollback(defStatus, false);
// return之后,虽然不会报错,但也不会提交事务
return;
}
// defStatus的transaction已经被hello方法设置为true,所以会走进这个分支
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
// 第二个参数为true,抛出异常咯!!
processRollback(defStatus, true);
return;
}
processCommit(defStatus);
}
这时候再走到processRollback
方法需要注意两点
- 第二个参数为ture,会抛出
UnexpectedRollbackException
异常,前文已解释过不再赘述 - 这次走的if…else分支不同于
Bservice#hello
方法,因为AService#doSomeThing
是开启了一个新事务,而Bservice#hello
方法是加入到了AService#doSomeThing
事务当中
else if (status.isNewTransaction()) {
doRollback(status);
}
// org.springframework.jdbc.datasource.DataSourceTransactionManager#doRollback
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject)status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
try {
// 真正回归事务
con.rollback();
} catch (SQLException var5) {
throw this.translateException("JDBC rollback", var5);
}
}
至此为止,在使用Transactional时抛出UnexpectedRollbackException
的原因就已经说完了。并且事务如何回滚的代码也讲到了,就是在doRollback方法中执行con.rollback();方法,通过sql的Connection来回滚代码的。
也就好理解了为什么TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
这行代码能够解决UnexpectedRollbackException
的出现。不过它只能阻止异常的出现,事务还是要回滚的。
转载自:https://juejin.cn/post/7294468438391980083