前言

最近项目上有一个使用业务相对复杂的业务场景报错了。在绝大多数情况下,都是风平浪静,没有问题。其实内涵暗潮涌动,在有些反常情况下就会报错,这种偶然性的问题很有可能就会在暴露到生产上造成事端,那究竟是怎样回事呢?

问题描述

咱们用一个简略的例子模拟下,咱们也能够看看下面这段代码输出的成果是什么。

  1. 在类SecondTransactionService界说一个简略接口transaction2,刺进一个用户,一起必然会抛出过错
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction2() {
    System.out.println("do transaction2.....");
    User user = new User("tx2", "111", 18);
    // 刺进一个用户
    userService.insertUser(user);
    // 跑错了
    throw new RuntimeException();
}
  1. 在别的一个类FirstTransactionService界说一个接口transaction1,它调用transaction2办法,一起做了try catch处理
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction1() {
    System.out.println("do transaction1 .......");
    try {
        // 调用别的一个业务,try catch住
        secondTransactionService.transaction2();
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 刺进当时用户tx1
    User user = new User("tx1", "111", 18);
    userService.insertUser(user);
}
  1. 界说一个controller,调用transaction1办法
@GetMapping("/testNestedTx")
public String testNestedTx() {
    firstTransactionService.transaction1();
    return "success";
}

咱们觉得调用这个http接口,终究数据库刺进的是几条数据呢?

问题成果

正确答案是数据库刺进了0条数据。

Spring中事务嵌套使用一定得警惕这个问题了!!

一起操控台也报错了,报错原因是:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

Spring中事务嵌套使用一定得警惕这个问题了!!

是否和你料想的一样呢?你知道是为什么吗?

原因追溯

其实原因很简略,咱们都知道,一个业务要么全成功提交业务,要么失利悉数回滚。假如出现在一个业务中部分SQL要回滚,部分SQL要提交,这不就主打的一个”前后矛盾,精神分裂“吗?

controller.testNestedTx()
  || 
  / 
FirstTransactionService.transaction1()   REQUIRED阻隔等级
       || 
       || 
       || 捕获反常,提交业务,犯错啦
       / || 
FirstTransactionService.transaction2()   REQUIRED阻隔等级
       || || 
       || 抛出反常,符号业务为rollback only
       =======================
  1. 业务的阻隔等级为REQUIRED,那么发现没有业务开启一个业务操作,有的话,就合并到这个业务中,所以transaction1()transaction2()是在同一个业务中。
  2. transaction2()抛出反常,那么业务会被符号为rollback only, 源码如下所示:

Spring中事务嵌套使用一定得警惕这个问题了!!

  1. transaction1()由于try catch 反常,正常运转,想必就要能够提交业务了,在提交业务的时分,会检查rollback符号,假如是true, 这时分就会抛出上面的反常了。源码如下图所示:

Spring中事务嵌套使用一定得警惕这个问题了!!
这下,是不是很清楚知道报错的原因了,那想想该怎样处理呢?

处理之道

知道了根本原因之后,是不是处理的方案就很明亮了,咱们能够通过调整业务的传达方法分拆多个业务管理,或许让一个业务”前后一致”,做一个诚信的好业务。

  • try catch放到内层业务中,也便是transaction2()办法中,这样内层业务会跟着外部业务进行提交或许回滚。
@Override
    @Transactional(rollbackFor = Exception.class)
    public void transaction2() {
        try {
            System.out.println("do transaction2.....");
            User user = new User("tx2", "111", 18);
            userService.insertUser2(user);
            throw new RuntimeException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 假如期望内层业务抛出反常时中断程序履行,直接在外层业务的catch代码块中抛出e,这样同一个业务就都会回滚。
  • 假如期望内层业务回滚,但不影响外层业务提交,需要将内层业务的传达方法指定为PROPAGATION_NESTEDPROPAGATION_NESTED基于数据库savepoint实现的嵌套业务,外层业务的提交和回滚能够操控嵌内层业务,而内层业务报错时,能够回来原始savepoint,外层业务能够继续提交。

Spring中事务嵌套使用一定得警惕这个问题了!!

业务的传达机制

前面提到了业务的传达机制,咱们再看都有哪几种。

  • PROPAGATION_REQUIRED:加入到当时业务中,假如当时没有业务,就新建一个业务。这是最常见的选择,也是Spring中默认采用的方法。
  • PROPAGATION_SUPPORTS:支撑当时业务,假如当时没有业务,就以非业务方法履行。
  • PROPAGATION_MANDATORY :支撑当时业务,假如当时没有业务,就抛出反常。
  • PROPAGATION_REQUIRES_NEW:新建一个业务,假如当时存在业务,把当时业务挂起。
  • PROPAGATION_NOT_SUPPORTED :以非业务方法履行操作,假如当时存在业务,就把当时业务挂起。
  • PROPAGATION_NEVER: 以非业务方法履行,假如当时存在业务,则抛出反常。
  • PROPAGATION_NESTED :假如当时存在业务,则在嵌套业务内履行。假如当时没有业务,则进行与PROPAGATION_REQUIRED类似的操作。

怎么理解PROPAGATION_NESTED的传达机制呢,和PROPAGATION_REQUIRES_NEW又有什么区别呢?咱们用一个例子说明白。

  • 界说serviceA.methodA()PROPAGATION_REQUIRED润饰;
  • 界说serviceB.methodB()以表格中三种方法润饰;
  • methodA中调用methodB;

Spring中事务嵌套使用一定得警惕这个问题了!!

总结

在我的项目中之所以会报“rollback-only”反常的根本原因是代码风格不一致的原因。外层业务对过错的处理方法是回来true或false来告知上游履行成果,而内层业务是通过抛出反常来告知上游(这里指外层业务)履行成果,这种差异就导致了“rollback-only”反常。咱们也能够去review自己项目中的代码,是不是也偷偷犯下同样的过错了。

欢迎重视个人公众号【JAVA旭阳】交流学习