工作原因是,摸鱼的时分在某渠道刷到一篇spring业务相关的博文,文章最后贴了一张图。里边关于嵌套业务的表述显着是过错的。

更奇怪的是,这张图有点形象。在必应搜索要害词PROPAGATION_NESTED出来的榜首篇文章,里边就有这这部份内容,也是结尾部份完全如出一辙。

更要害的是,人家原文是表格,这位倒好,估计是怕麻烦,直接给截成图片了。

而且这篇文章其实在评论区现已被人指出来这方面的问题了,谁也不能保证自己写的文章没有一点纰漏,改了不就好了。但原作者并没有加以理会并修改过错。 一起,这位转载作者依然不加验证的直接拿走了。

这位转载作者可不是个小号,是某年度的人气作者。
可能是有自己的大众号,得保持必定的更新频率?


关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹


好家伙,没经过验证,一部份过错的内容就这样被持续扩展传达了。

在必应搜索要害词PROPAGATION_NESTED出来文章,前两篇都是CSDN,都是相同的文章相同的过错。另外几篇文章也或多或少有些表述不清的当地。因而尝试来写一写这方面的东西。

趁便吐槽一下CSDN,我好多篇文章都被这上面的某些作者给扒曩昔,然后搜索如出一辙的标题,权重比我还高,出来排榜首位的反而是CSDN的盗版文章。

1.当咱们在议论嵌套业务的时分,嵌套的是什么?


当看到`嵌套业务`榜首反响想到是这款式的:

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

但这更像PROPAGATION_REQUIRES_NEW啊,感兴趣能够去打断点履行一下。PROPAGATION_REQUIRES_NEW业务传达下,办法A调用办法B便是这样,

//        业务A doBegin()
//            业务B doBegin()
//            业务B doCommit()
//        业务A doCommit()

而在PROPAGATION_NESTED业务传达下,打了个断点,会发现只会履行一次doBegin和doCommit:

业务A doBegin()
业务A doCommit()

咱们用代码输出愈加直观。
界说两个办法serviceA和serviceB,运用前者调用后者。前者业务传达运用REQUIRED,后者运用PROPAGATION_NESTED

@Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("测验城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            test2.serviceB();
    }
 @Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
    public void serviceB() {
        Tcity tcity = new Tcity();
        tcity.setId(0);
        tcity.setStateCode("5");
        tcity.setCnCity("测验城市");
        tcity.setCountryCode("ALB");
        tcityMapper.insertSelective(tcity);
        tcityMapper.selectAll2();
        transactionInfo();

这儿的transactionInfo()运用业务同步器管理器TransactionSynchronizationManager注册一个业务同步器TransactionSynchronization
这样在业务完成之后afterCompletion会输出当时业务是commit还是rollback,这样也便于测验,比起去刷新数据库看有没有写入,愈加方便快捷直观。

一起运用TransactionSynchronizationManager.getCurrentTransactionName()能够得到当时业务的称号,这样能够直观的看到当时办法运用的是同一个业务还是不同的业务。

protected void transactionInfo() {
        String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean active = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("transactionName:{}, active:{}", transactionName, active);
        if (!active) {
            log.info("transaction :{} not active", transactionName);
            return;
        }
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_COMMITTED) {
                    log.info("transaction :{} commit", transactionName);
                } else if (status == STATUS_ROLLED_BACK) {
                    log.info("transaction :{} rollback", transactionName);
                } else {
                    log.info("transaction :{}  unknown", transactionName);
                }
            }
        });
    }

履行测验代码:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test { 
    @Autowired
    private Test1 test1;
    @org.junit.Test
    public void test(){
        test1.serviceA();
    }
}

输出:

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

能够非常直观地观察到3点状况: 1.经过上图标记为1的当地,能够看到两个办法运用了一个业务com.nyp.test.service.propagation.Test1.serviceA
2.经过上图标记为2的当地,以及箭头次序,能够看到业务履行次序类似于(事实上不是,只是业务同步器的问题,下文有阐明):

//        业务A doBegin()
//            业务B doBegin()
//        业务A doCommit()
//            业务B doCommit()

3.经过业务同步器打印日志发现commit履行了两次。

以上2,3两点与前面打断点的定论貌似是有点冲突。


1.1嵌套业务终究有几个业务


源码版别:spring-tx 5.3.25

经过源码,能够很直观地观察到,useSavepointForNestedTransaction()默许回来true,这样就不会敞开一个新的业务(startTransaction), 而是创立一个新的savepoint

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

相当于在办法A的时分会敞开一个新的业务,在调用办法B的时分,会在办法A之后办法B之前创立一个检查点。

类似于在本来的A办法上手动添加检查点。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Object savePoint = null;
        try {
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("测验城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        }
    }


然后经过检查点,将一个逻辑业务分为多个物理业务
我这可不是在乱讲啊,我是有备而来。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹


github.com/spring-proj…
上面是spring 在github官方社区07年的一个贴子,Juergen Hoeller有一段回复。

Juergen Hoeller是谁?他是spring的联合创始人,业务这一块的首要开发者。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹


PROPAGATION_NESTED的不同之处在于,它运用具有多个保存点的单个物理业务,能够回滚到这些保存点。这种部分回滚允许内部业务规模触发其规模的回滚,而外部业务能够持续进行物理业务,虽然现已回滚了一些操作。这通常映射到JDBC保存点上,因而只适用于JDBC资源业务(Spring的DataSourceTransactionManager)。

在嵌套业务中,全体是一个逻辑业务,经过savepoint在jdbc物理层面把调用办法分割成一个个的物理业务。
由于spring层面只有一个逻辑业务,所以经过断点只履行了一次doBegin()和doCommit(),但实际上履行了两次preCommit(),假如有savepoint那就不履行commit(),
这也能答复上面2,3两点问题的疑问。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

所以上面办法A调用办法B进行嵌套业务,右图比左图更形象精确:

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹
关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

1.2 savepoint

savepoint是JDBC的一种机制,spring运用savepoint来完成了嵌套业务。
在数据库操作中,默许autocommit为true,意味着一条SQL一个业务。也能够将autocommit设置为false,将多条SQL组成一个业务,一起commit或者rollback。
以上都是惯例操作,在一个业务中所以数据库操作全部捆绑在一起。在某些特定状况下,在一个业务中,用户只希望rollback其中某部份,这时分能够用到savepoint。


记咱们遗忘@Transactional,以编程式业务的方式来手动设置一个savepoint。

办法A,写入一条用户记载,并设置一个检查点。

    @Autowired
    private PlatformTransactionManager platformTransactionManager;
    public void serviceA(){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        Object savePoint = null;
        try {
            Person person = new Person();
            person.setName("张三");
            personDao.insertSelective(person);
            transactionInfo();
            // 设置一个savepoint
            savePoint = status.createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            // 这儿输出两次commit,到rollback到51行,会插入一条数据
            status.rollbackToSavepoint(savePoint);
            // 这儿会两次rollback
//            platformTransactionManager.rollback(status);
        }
        platformTransactionManager.commit(status);
    }

办法B写入一条日志记载。并在此模仿一个反常。

    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);     
        int a = 1 / 0;
    }

测验希望到达的作用是,日志写入失利,但用户记载写入成功。很显着,假如不运用savepoint是达不到的。由于两个办法是一个业务,在办法B中报错了,抛出反常,用户和日志的数据库操作都将回滚。

测验输出日志:

[2023-04-24 14:40:18.740] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transactionName:null, active:true
[2023-04-24 14:40:18.742] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transactionName:null, active:true
java.lang.ArithmeticException: / by zero
	......省略
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transaction :null commit
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transaction :null commit

数据库也表明用户写入成功,日志写入失利。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹


2.一开始的问题,B先回滚A再正常提交?

本文开始的问题是办法A业务传达为PROPAGATION_REQUIRED,办法B业务传达为PROPAGATION_NESTED。办法A调用B,methodA正常,methodB抛反常。
这种状况下会发生什么?

B先回滚,A再正常提交这种说法为什么会有问题,有什么问题?

2.1 先B后A的次序有问题吗?

经过前面业务同步器打印的日志咱们得知,业务以test1.serviceA()履行doBegin(),test2.serviceB()履行doBegin(),test1.serviceA()履行doCommit(),test2.serviceB()履行doCommit()这样的次序履行。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹
可是果真如此吗?

经过源码咱们首先得知,preCommit()在commit()办法之前,在preCommit()会做savepoint的判别,假如有检查点就不履行commit()。

  1. 一起办法B只是一个savepoint不是一个真实的业务,并不会履行业务同步器。
    关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹
  2. 办法A是一个真实的业务,所以会履行commit(),一起也会履行上面的业务同步器。

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

这儿的业务同步器是一个Arraylist,它的履行次序即是arraylist的遍历次序,只是只代表参加的先后,并不代表业务真实commit/rollback的次序。


从1,2两点能够得出定论,先B后A的次序并没有问题。

一起,依据1,在嵌套业务中运用业务同步器要特别当心,在检查点的时分并不会履行同步器,一起会掩盖真实的操作。

比如办法B回滚了,但由于办法B只是个savepoint,所以业务同步器不会履行。等到办法A履行完操作业务同步器的时分,也只会反响外层业务即办法A的业务成果。


2.2 真实的问题

假如B回滚,A是commit还是rollback取决于办法A是否持续把反常往上抛。


让咱们先暂时遗忘嵌套业务,测验一个REQUIRES_NEW的事例。
同样的办法A业务传达为REQUIRES,办法B为REQUIRES_NEW。 此刻办法A和办法B为两个互相独立的业务。 办法A调用办法B,办法B抛出反常。
此刻,办法B肯定会回滚,但办法A呢?按理说互相独立,那肯定是commit了。

但真的如此吗?
(1). 办法A不做反常处理。

测验成果:

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

能够看到确实是两个业务,但两个业务都rollback了。由于办法A虽然没有报反常,但它接到了办法B的反常且往上抛了,spring只会以为办法A同样也抛出了反常。因而两个业务都需要回滚。

(2).办法A处理了反常。

将办法A代码try-catch住,再履行。

日志有点多不做截图,

[2023-04-24 16:10:30.669] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transactionName:com.nyp.test.service.propagation.Test1.serviceA, active:true
[2023-04-24 16:10:30.672] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transactionName:com.nyp.test.service.propagation.Test2.serviceB, active:true
[2023-04-24 16:10:30.687] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transaction :com.nyp.test.service.propagation.Test2.serviceB rollback
java.lang.ArithmeticException: / by zero
	 省略
[2023-04-24 16:10:30.689] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transaction :com.nyp.test.service.propagation.Test1.serviceA commit

能够看到两个单独的业务,业务B回滚了,业务A提交了。

虽然咱们这末节说的是REQUIRES_NEW,但嵌套业务是相同的道理。

假如B回滚,当办法A持续往上抛反常,则A回滚;当办法A处理了反常不往上抛,则A提交。

3. 场景

在2.2末节中,咱们举了REQUIRES_NEW的例子来阐明,有的同学可能就会有点疑问了。既然业务B回滚了,业务A都要依据状况来判别是否回滚,那这样嵌套业务跟REQUIRES_NEW有啥区别?

还是拿注册的场景来说。往数据库写1条用户记载,再写1条注册成功操作日志。

  1. 假如日志写入失利,用户写入不受影响。这种状况下, REQUIRES_NEW和嵌套业务都能完成。而且很显着REQUIRES_NEW还没那么弯弯绕绕。

  2. 考虑另外一种状况,假如用户写入失利了,那这时分我想要日志写入也失利。由于用户都没了,就不存在注册操作成功的操作日志了。

场景1

办法A传达级别为REQUIRED,并模仿一个反常。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Person person = new Person();
        person.setName("李四");
        personDao.insertSelective(person);
        transactionInfo();
        test2.serviceB();
        int a = 1 / 0;
    }

在办法B为REQUIRES_NEW

    @Transactional(propagation = Propagation.REQUIRED_NEW)
    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);
    }

打印输出

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

能够看到办法B提交了,也便是说用户注册失利了,但用户注册成功的操作日志却写入成功了。


场景2

咱们再来看看嵌套业务的状况下: 办法A传达级别为REQUIRED,并模仿一个反常。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Person person = new Person();
        person.setName("李四");
        personDao.insertSelective(person);
        transactionInfo();
        test2.serviceB();
        int a = 1 / 0;
    }

办法B业务传达级别为NESTED。

    @Transactional(propagation = Propagation.NESTED)
    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);
    }

履行日志

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

能够看到同一个逻辑业务下的两段物理业务都回滚了,到达了咱们预期的作用。

4.小结

1.办法A业务传达为REQUIRED,办法B业务传达为NESTED。办法A调用办法B,当B抛出反常时,
假如A处理了反常,此刻业务A提交。否则,业务A回滚。

2.REQUIRED_NEW和NESTED在有些场景下能够完成相同的功能,但在某些特定场景下只能NESTED完成。
3.NESTED底层逻辑是JDBC的savepoint。父业务类似于一个逻辑业务,savepoint将各办法分割了若干物理业务。
4.在嵌套业务中运用业务同步器时需要特别当心。


看到这儿点个赞呗`

关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹