vlambda博客
学习文章列表

一次bug修复,重新认识spring事务传播机制—破案篇

前段时间,QA在jira提了个bug,偶现一个问题。报错:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

很遗憾,当时没有记录整个bug修复过程,现在那段代码也让我替换掉了。

不过在解决的过程中,让我对spring事务传播机制以及spring事务又多了一份了解。

当时出现这个没见过的异常应该是,类A的一个方法需要用事务处理,在调用B的时候,没有注意B的方法有事务,但是想对B的调用异常结果做额外业务处理,就加了try catch。

今天按照这个场景还原一下 ”案发现场“。



  • 事务A
/**
 *  事务A
 * created by mayibz on 2021/1/15
 */
@Service
@Slf4j
public class TransactionA {
    @Autowired
    private TransactionB transactionB;
    @Autowired
    private TestTransactionMapper testTransactionMapper;

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void methodA() {
        log.info("开始执行事务A");
        testTransactionMapper.insertRecord("test record");
        try {
            transactionB.methodB();
        }catch (Exception e) {
            log.info("事务B执行异常,不打印异常栈");
        }
        log.info("事务A执行完毕");
    }
}

  • 事务B
/**
 * 事务B
 * created by mayibz on 2021/1/15
 */
@Service
@Slf4j
public class TransactionB {
    @Transactional(rollbackFor = Exception.class)
    public void methodB() {
        log.info("开始执行事务B");
        throw new RuntimeException("执行事务B异常");
    }
}

  • 从事务A开始调用
@Test
public void test() {
    transactionA.methodA();
}

如果这样做,你觉得会出现什么情况呢?

如果只限理论学习,或者不够深入,很容易给出如下结论:

  • 日志只会输出 :开始执行事务A, 开始执行事务B, 事务B执行异常,不打印异常栈,事务A执行完毕
  • 事务A成功插入一条record

然而,事实并非如此:


一次bug修复,重新认识spring事务传播机制—破案篇


一次bug修复,重新认识spring事务传播机制—破案篇

那么就让我们带着问题,使用debug,福尔摩斯的华生!走进科学(源码)!!!

一次bug修复,重新认识spring事务传播机制—破案篇

注解开启的事务,当然是通过切面,然后进入到spring 提供的 TransactionAspectSupport 类中。


一次bug修复,重新认识spring事务传播机制—破案篇首先执行事务A,即插入一条recod,然后调用事务B。

一次bug修复,重新认识spring事务传播机制—破案篇

毫无疑问,事务B会抛出异常。

调用processRollback(), 处理事务B的回滚。这是事务B的当前事务状态(这块已经出现了savepoint的身影,spring处理嵌套事务的关键)


一次bug修复,重新认识spring事务传播机制—破案篇

一次bug修复,重新认识spring事务传播机制—破案篇


一次bug修复,重新认识spring事务传播机制—破案篇

然后根据当前状态值,在这一步,通过doSertRollbackOnly() 调用 数据库的rollback,进行事务B的回滚。

protected void doSetRollbackOnly(DefaultTransactionStatus status) {
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
   if (status.isDebug()) {
      logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() +
            "] rollback-only");
   }
   txObject.setRollbackOnly();
}

记住这个方法,doSetRollbackOnly()案情突破的伏笔!


一次bug修复,重新认识spring事务传播机制—破案篇


最后在这两步操作,抛出了当时出现的错误异常!


一次bug修复,重新认识spring事务传播机制—破案篇

然后,事务B执行完毕,抛出我们代码中定义的异常

一次bug修复,重新认识spring事务传播机制—破案篇

再被我们的try catch 代码捕获

一次bug修复,重新认识spring事务传播机制—破案篇

然后到事务A,执行后续的代码

一次bug修复,重新认识spring事务传播机制—破案篇

事务A后续代码执行完毕后,准备执行事务提交

一次bug修复,重新认识spring事务传播机制—破案篇

一次bug修复,重新认识spring事务传播机制—破案篇

shoulodCommitOnGlobalRollbackOnly() 默认是true,
所以也就是说 因为事务A的 isGlobalRollbackOnly() 返回的是ture,表示事务被标记了全局回滚,
导致了事务A也进行了回滚。 

追踪isGlobalRollbackOnly()这个方法,来到了 ResourceHoderSupportrollbackOnly 属性。

一次bug修复,重新认识spring事务传播机制—破案篇


然后又一路找set这个字段的方法调用,最终发现就是我们事务B在执行processRollback() 方法的时候,也就是上面提到的伏笔,将事务标记了全局回滚!!!

一次bug修复,重新认识spring事务传播机制—破案篇


也正是因为事务B标记了全局回滚,

才会抛出  UnexpectedRollbackException


一次bug修复,重新认识spring事务传播机制—破案篇

至此,结案。撒花~~~一次bug修复,重新认识spring事务传播机制—破案篇一次bug修复,重新认识spring事务传播机制—破案篇一次bug修复,重新认识spring事务传播机制—破案篇一次bug修复,重新认识spring事务传播机制—破案篇一次bug修复,重新认识spring事务传播机制—破案篇


结案报告:

事务A先执行插入数据库一条记录操作,然后调用事务B,事务B发生异常。

spring在按要求对事务B回滚时,除了对事务B进行回滚外,还设置了全局回滚点为true, 导致抛出UnexpectedRollbackException

虽然A中捕获了B的异常,但是事务A在提交时,发现全局回滚点为true,也进行了回滚操作。

ps

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

这个异常最终是由A事务抛出的,而不是B事务。
B事务在处理回滚时最后一步抛出的是当时本代码定义的异常。

并且在事务A中捕获的异常级别是Exception级别, 我们在case中并未打印异常栈。



回过头来再想,我们如果事务B为默认传播机制,即REQUIRED ,表明假如在父事务A中嵌套执行,应该同事务A作为一个整体事务来执行,所以B回滚,A回滚; A回滚,B回滚。 这也是spring期望正确使用这个传播机制的姿势。

即使期望事务B的执行不干扰事务A的commit,那也不应该用 try catch这种方式去处理,那应该如何处理呢? 敬请期待下一篇《原理篇》!