org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it h...

今天线上出现了一个业务功能异常,在排查日志的时候发现了诡异的异常信息:

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,外层事务可以继续提交。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。