今天线上出现了一个业务功能异常,在排查日志的时候发现了诡异的异常信息:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:728) ~[spring-tx-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:518) ~[spring-tx-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292) ~[spring-tx-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.20.RELEASE.jar:4.3.20.RELEASE]
根据异常堆栈,我们定位到Spring的AbstractPlatformTransactionManager的commit方法,具体代码如下:
@Override
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);
}
我们来简单的分析一下commit所做的事情,commit会在我们代码中承担提交事务的操作。异常信息来源于status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()
。即对事务状态的检测以及事务内是否有rollback。我们使用的@Transactional提供的默认事务传播行为,即Propagation.REQUIRED,如果不存在事务那么会开启事务。那么第一个条件是不满足的,我们再来重点关注一下isFailEarlyOnGlobalRollbackOnly
。该方法实际是返回的failEarlyOnGlobalRollbackOnly属性。我们来看一下对failEarlyOnGlobalRollbackOnly属性的描述:
/**
* Set whether to fail early in case of the transaction being globally marked
* as rollback-only.
* <p>Default is "false", only causing an UnexpectedRollbackException at the
* outermost transaction boundary. Switch this flag on to cause an
* UnexpectedRollbackException as early as the global rollback-only marker
* has been first detected, even from within an inner transaction boundary.
* <p>Note that, as of Spring 2.0, the fail-early behavior for global
* rollback-only markers has been unified: All transaction managers will by
* default only cause UnexpectedRollbackException at the outermost transaction
* boundary. This allows, for example, to continue unit tests even after an
* operation failed and the transaction will never be completed. All transaction
* managers will only fail earlier if this flag has explicitly been set to "true".
* @since 2.0
* @see org.springframework.transaction.UnexpectedRollbackException
*/
public final void setFailEarlyOnGlobalRollbackOnly(boolean failEarlyOnGlobalRollbackOnly) {
this.failEarlyOnGlobalRollbackOnly = failEarlyOnGlobalRollbackOnly;
}
简单而言就是:在提交此事务之前是否已经发生过事务失败而被标记为只能rollback。
这句话很好解释,我们的事务一般我们都会使用默认的传播方式,这样无论外层事务和内层事务任何一个出现异常,那么所有的sql都不会执行。在嵌套事务场景中,内层事务的sql和外层事务的sql会在外层事务结束时进行提交或回滚。如果内层事务抛出异常e,在内层事务结束时,spring会把事务标记为“rollback-only”。这时如果外层事务捕捉了异常e,那么外层事务方法还会继续执行代码,直到外层事务也结束时,此时由于内部失败,那么Spring会当做无法提交事务处理,以保证我们整个事务内的原子性。
简单的复现的示例代码如下:
Class A {
@Resource
private B b;
@Transactional
public void a() {
try {
b.b()
} catch (Exception ignore) {
}
}
}
Class B {
@Transactional
public void b() {
throw new RuntimeException();
}
}
那么我们在执行此a()方法的时候就会出现上述异常。
解决办法:
- 如果想让外部事务被中断,那么我们可以手动将catch的异常再抛出。
- 内部的事务的异常我们手动捕获,不抛出异常
- 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为PROPAGATION_NESTED。注:PROPAGATION_NESTED基于数据库savepoint实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事务,而内层事务报错时,可以返回原始savepoint,外层事务可以继续提交。