【线上问题】Transaction rolled back because it has been marked as rollback-only 是怎么回事?

1,189 阅读4分钟

一、问题描述

线上的日志中偶然排查到一个事务相关问题,头一次看到这个异常,一起来学习下。

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();
    }
}

运行后控制台结果:

image.png

二、原因分析:

当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();
    }
}

运行结果:

image.png

传播级别功能描述
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("捕获异常");
        }
    }
}

image.png


By the way:

Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:

  1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

  3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

  4. Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

  5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

  7. Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。