Spring事务方法嵌套引发的异常与问题分析

1 案例背景

由于Spring框架优秀的特性,很多java项目都会集成Spring。其中,Spring的轻量级声明式事务管理极大的简化了开发者对于事务的开发工作。

而我在最近的项目开发过程中,遇到了一个奇怪的事务异常:Transaction rolled back because it has been marked as rollback-only。

这篇案例将会对代码逻辑进行排查,同时对rollback-only异常进行分析。

2 案例分析

为了排查和分析rollback-only异常产生的原因,下面我会通过一个demo来模拟上述方法的调用过程。

2.1 测试用例

项目使用Springboot作为启动方式,Spring进行事务管理,Service层作为AOP事务控制的切入点。事务方法的传播属性为REQUIRED,即表示当前方法必须在一个事务中运行。如果已存在一事务正在进行,则该方法将在现有事务中运行,否则就要开始一个新事务。

为了便于理解与分析,这里简化了引发异常的业务代码,只保留关键部分。

调用过程:

事务配置类TransactionAdviceConfig.java

@Aspect
@Configuration
public class TransactionAdviceConfig {
    private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.hikvision.pbg.cp.ipts.web.modules.service..*.*(..)) ";
    @Autowired
    DataSource dataSource;
​
    @Bean
    public TransactionInterceptor txAdvice() {
        DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute();
        txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        source.addTransactionalMethod("save*", txAttrRequired);
        source.addTransactionalMethod("delete*", txAttrRequired);
        source.addTransactionalMethod("remove*", txAttrRequired);
        source.addTransactionalMethod("update*", txAttrRequired);
        return new TransactionInterceptor(transactionManager(), source);
    }
    @Bean
    public Advisor txAdviceAdvisor() {
        // TODO new  Advisor
    }
    @Bean
    public DataSourceTransactionManager transactionManager() {
        // TODO new DataSourceTransactionManager
    }
}


AlarmServiceTest.java


@RunWith(SpringRunner.class)
@SpringBootTest(value = "application-dev.properties")
public class AlarmServiceTest {
    @Autowired
    private IAlarmService alarmService;
    @Test
    public void rollbackTest() throws Exception {
        alarmService.saveTest();
        System.out.println("AlarmServiceTest rollbackTest success!!!");
    }
}

IAlarmService.java

@Service
public class AlarmServiceImpl implements IAlarmService{
    @Autowired
    private IPivotService pivotService;
    @Override
    public void saveTest() {
        try{
            pivotService.updateTest();
        }catch (Exception e){
            // TODO nothing
        }
        System.out.println("AlarmServiceImpl saveTest success!!!");
    }
}

IPivotService.java

@Service
public class PivotServiceImpl implements IPivotService{
    @Override
    public void updateTest() {
        throw new RuntimeException("Rolling Back Test");
    }
}

AlarmServiceTest.rollbackTest()作为Junit测试用例,不受事务控制;alarmService.saveTest()和pivotService.updateTest()进行事务控制。

执行AlarmServiceTest.rollbackTest()方法,结果如下

果不其然,执行结果抛出了rollback-only异常。

2.2 用例分析

2.2.1 排查日志

根据日志打印的结果来看,rollback-only异常是在alarmService.saveTest()方法执行成功后发生的。细究异常堆栈信息,最后抛出异常的地方定位于AbstractPlatformTransactionManager.commit方法第728行,位于springframework.transaction jar包下。

at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:728)

2.2.2 源码分析

  • 查看springframework源码AbstractPlatformTransactionManager.commit()
/**
* This implementation of commit handles participating in existing
* transactions and programmatic rollback requests.
* Delegates to {@code isRollbackOnly}, {@code doCommit}
* and {@code rollback}.
* @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
* @see #doCommit
* @see #rollback
*/
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;
        if (defStatus.isLocalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Transactional code has requested rollback");
            }
            processRollback(defStatus);
            return;
        }
        if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
            }
            processRollback(defStatus);
            // Throw UnexpectedRollbackException only at outermost transaction boundary
            // or if explicitly asked to.
            if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                throw new UnexpectedRollbackException(
                        "Transaction rolled back because it has been marked as rollback-only");
            }
            return;
        }
        processCommit(defStatus);
    }

显然,根据方法名和源码中的注释可知,这是一段Spring事务提交的逻辑。

定位Spring源码,异常抛出位置如下

此处可以看出有两个if判断,这是引起rollback-only异常的判断条件。

第一个if语句

if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())
  • shouldCommitOnGlobalRollbackOnly默认实现是false。这里是指如果发现事务被标记全局回滚并且在全局回滚标记情况下不应该提交事务的话,那么则进行回滚。
  • defStatus.isGlobalRollbackOnly()进行判断是指读取DefaultTransactionStatus中transaction对象的ConnectionHolder的rollbackOnly标志位

这句if判断结果为true的含义是全局事务标记为回滚,但事务代码却请求提交

第二个if语句

if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly())

这里isFailEarlyOnGlobalRollbackOnly()=false,而failEarlyOnGlobalRollbackOnly是一个标志位,默认情况下failEarlyOnGlobalRollbackOnly开关是关闭的。这个开关的作用是如果开启了程序则会尽早抛出异常。

所以,status.isNewTransaction()这个方法返回的参数起着是否抛出rollback-only异常的关键判断。

查看status.isNewTransaction()实现

private final boolean newTransaction;
public boolean hasTransaction() {
	return (this.transaction != null);
}
@Override
public boolean isNewTransaction() {
    return (hasTransaction() && this.newTransaction);
}

阅读isNewTransaction()源码注释:返回当前事务是否为新事务。大概意思就是true表示当前是一个新事务,false表示参与现有事务或不在当前事务中。

  • 继续向上查看源码 TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
	TransactionAttributeSource tas = getTransactionAttributeSource();
	final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);// 1
	final PlatformTransactionManager tm = determineTransactionManager(txAttr);
	final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    // 1.1 是否是声明式事务
	if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
		TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
		Object retVal = null;
		try {
			retVal = invocation.proceedWithInvocation();// 2
		}
		catch (Throwable ex) {
			completeTransactionAfterThrowing(txInfo, ex);// 3
			throw ex;
		}finally {
			cleanupTransactionInfo(txInfo);
		}
		commitTransactionAfterReturning(txInfo);// 4
		return retVal;
	}
	else {
         // 1.2 编程式事务
		// 省略...
	}
}

这里,为了理清源码逻辑,我只列出了关键步骤并标记序号:

  1. 获取事务传播属性,也就是我们业务代码中的PROPAGATION_REQUIRED
  2. 执行事务方法
  3. 捕获异常,并将会把事务设置为Rollback回滚状态。
  4. 提交事务

这一段Spring的源码还是比较清晰的,所有的事务方法都是通过Spring AOP代理执行的,也就是源码中第2步invocation.proceedWithInvocation()方法。在alarmService.saveTest()方法调用pivotService.updateTest()方法时,pivotService.updateTest()方法会经过一次Spring AOP事务切面,事务切面逻辑里Spring自己就会对pivotService.updateTest()方法进行异常捕获。

  • 事务属性分析
    回到isNewTransaction()作用的问题。

前面讲到,TransactionAdviceConfig事务配置类,Spring执行service事务方法的事务传播属性为REQUIRED,这两个事务方法都会在同一事务里。根据这个逻辑,可猜测alarmService.saveTest()中isNewTransaction()应该为true,而pivotService.updateTest()方法中isNewTransaction()应该为false,因为alarmService.saveTest()是事务控制里第一个执行的事务方法。

接下来,Debug调试查看这两个Serivce的事务方法isNewTransaction()的返回值。

执行alarmService.saveTest()

执行pivotService.updateTest()

调试结果正如预期。

那么,为什么要在事务状态为rollback时判断status.isNewTransaction()呢?status.isNewTransaction()=true在这里又是表示何意呢?

AbstractPlatformTransactionManager.commit()源码中有一句注释,大概意思是当在最外层事务边界处或显式要求时,才会引发UnexpectedRollbackException

这段注释说明可能不太好理解。拿本次案例的用例来说,事务传播属性都为REQUIRE,事务方法调另一个事务方法,第二个方法抛出异常,第一个方法捕获后并执行完成,Spring认为这是到了最外层事务边界,因为只有第一个事务方法才会创建一个新的事务,内部调用的方法都会沿用已存在的事务。

  • 排查结果

根据事务执行逻辑并结合Spring源码可以判断,Spring捕获异常后,事务将会被设置全局rollback,而最外层的事务方法执行commit操作,这时由于事务状态为rollback,同时status.isNewTransaction()=true,说明执行流程已经到达该事务边界,这个时候Spring认为不应该commit提交事务,而应该回滚事务。所以,Spring会抛出rollback-only异常。

3 解决方案

既然Service方法不能捕获异常,只能向上继续抛出异常?那遇到例如事务方法中需要消化异常的业务场景不是无法实现了?

从上述分析看,产生rollback-only异常需要同时满足以下前提:

1.事务方法嵌套;

2.子方法抛出异常,被上层方法捕获和消化;

3.事务状态被设置为rollback状态;

4.执行流程已到达事务边界,即在同一事务中;

为了满足业务场景需要捕获消化异常,同时避免rollback-only异常发生。有以下几种解决方案:

3.1 捕获异常时,手动设置上层事务状态为rollback

修改alarmService.saveTest(),在catch代码块中加入

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

@Service
public class AlarmServiceImpl implements IAlarmService{
    @Autowired
    private IPivotService pivotService;
    @Override
    public void saveTest() {
        try{
            pivotService.updateTest();
        }catch (Exception e){
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        System.out.println("AlarmServiceImpl saveTest success!!!");
    }
}

执行结果

3.2 异常方法不受事务控制

pivotService.updateTest()修改为pivotService.editTest()

@Service
public class PivotServiceImpl implements IPivotService{
    @Override
    public void editTest() {
        throw new RuntimeException("Rolling Back Test");
    }
}

执行结果

3.3 事务传播属性修改为REQUIRES_NEW

将事务传播属性修改为REQUIRES_NEW,即新建事务,如果当前存在事务,把当前事务挂起

	@Bean
    public TransactionInterceptor txAdvice() {
        DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute();
        txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        source.addTransactionalMethod("save*", txAttrRequired);
        source.addTransactionalMethod("delete*", txAttrRequired);
        source.addTransactionalMethod("remove*", txAttrRequired);
        source.addTransactionalMethod("update*", txAttrRequired);
        return new TransactionInterceptor(transactionManager(), source);
    }

执行结果

上述三种解决方案都能避免异常的发生,但从软件开发与维护的角度考虑,既然事务方法catch异常并不回滚,那又何必进行事务控制。因此,项目中我采用了第二种方案处理异常情况。

4 总结

由于Spring框架隐藏了很多事务控制的细节和底层繁琐的逻辑,极大的减少了开发的复杂度。但是,如果我们对底层源码的思想多一些理解的话,对于问题排查和开发都会有更清晰的思路与收获。

通过这次问题排查与探究,我对于service层事务方法异常的处理,有几点编程建议:

1.当serivce层方法调用其他serivce方法时,若需要catch异常,建议catch不在事务控制范围里的service方法

2.当service层事务方法catch了其他service事务方法,则必须要向外层抛出异常

编辑于 2023-08-14 17:07・IP 属地未知