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

科技资讯 投稿 6700 0 评论

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

更奇怪的是,这张图有点印象。在必应搜索关键词PROPAGATION_NESTED出来的第一篇文章,里面就有这这部份内容,也是结尾部份完全一模一样。

而且这篇文章其实在评论区已经被人指出来这方面的问题了,但是这位作者依然不加验证的直接拿走了。

可能是有自己的公众号,得保持一定的更新频率?


在必应搜索关键词PROPAGATION_NESTED出来文章,前两篇都是CSDN,都是一样的文章一样的错误。另外几篇文章也或多或少有些表述不清的地方。因此尝试来写一写这方面的东西。

1.当我们在谈论嵌套事务的时候,嵌套的是什么?


当看到`嵌套事务`第一反应想到是这样式的:

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

输出:

1.通过上图标记为1的地方,可以看到两个方法使用了一个事务com.nyp.test.service.propagation.Test1.serviceA
2.通过上图标记为2的地方,以及箭头顺序,可以看到事务执行顺序类似于(事实上不是,只是事务同步器的问题,下文有说明):

//        事务A doBegin(
//            事务B doBegin(
//        事务A doCommit(
//            事务B doCommit(

3.通过事务同步器打印日志发现commit执行了两次。

1.1嵌套事务究竟有几个事务


源码版本:spring-tx 5.3.25

useSavepointForNestedTransaction(默认返回true,这样就不会开启一个新的事务(startTransaction, 而是创建一个新的savepoint

类似于在原来的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官方社区07年的一个贴子,Juergen Hoeller有一段回复。

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


在嵌套事务中,整体是一个逻辑事务,通过savepoint在jdbc物理层面把调用方法分割成一个个的物理事务。
因为spring层面只有一个逻辑事务,所以通过断点只执行了一次doBegin(和doCommit(,但实际上执行了两次preCommit(,如果有savepoint那就不执行commit(,
这也能回答上面2,3两点问题的疑问。

1.2 savepoint

在数据库操作中,默认autocommit为true,意味着一条SQL一个事务。也可以将autocommit设置为false,将多条SQL组成一个事务,一起commit或者rollback。
以上都是常规操作,在一个事务中所以数据库操作全部捆绑在一起。在某些特定情况下,在一个事务中,用户只希望rollback其中某部份,这时候可以用到savepoint。


@Transactional,以编程式事务的方式来手动设置一个savepoint。

    @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

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


2.一开始的问题,B先回滚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(这样的顺序执行。
但是果真如此吗?

    同时方法B只是一个savepoint不是一个真正的事务,并不会执行事务同步器。
  1. 方法A是一个真正的事务,所以会执行commit(,同时也会执行上面的事务同步器。

这里的事务同步器是一个Arraylist,它的执行顺序即是arraylist的遍历顺序,仅仅只代表加入的先后,并不代表事务真正commit/rollback的顺序。


从1,2两点可以得出结论,先B后A的顺序并没有问题。

比如方法B回滚了,但因为方法B只是个savepoint,所以事务同步器不会执行。等到方法A执行完操作事务同步器的时候,也只会反应外层事务即方法A的事务结果。


2.2 真正的问题

让我们先暂时忘掉嵌套事务,测试一个REQUIRES_NEW的案例。
同样的方法A事务传播为REQUIRES,方法B为REQUIRES_NEW
此时方法A和方法B为两个彼此独立的事务。
方法A调用方法B,方法B抛出异常。
此时,方法B肯定会回滚,但方法A呢?按理说彼此独立,那肯定是commit了。
但真的如此吗?
(1. 方法A不做异常处理。

(2.方法A处理了异常。

日志有点多不做截图,

[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. 场景

REQUIRES_NEW的例子来说明,有的同学可能就会有点疑问了。既然事务B回滚了,事务A都要根据情况来判断是否回滚,那这样嵌套事务跟REQUIRES_NEW有啥区别?

    如果日志写入失败,用户写入不受影响。这种情况下,REQUIRES_NEW和嵌套事务都能实现。而且很明显REQUIRES_NEW还没那么弯弯绕绕。
    2.考虑另外一种情况,如果用户写入失败了,那这时候我想要日志写入也失败。因为用户都没了,就不存在注册操作成功的操作日志了。

这种场景,在方法B为REQUIRES_NEW模式下,打印输出

我们再来看看嵌套事务的情况下:
方法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;
    }

执行日志

4.小结

1.方法A事务传播为REQUIRED,方法B事务传播为NESTED。方法A调用方法B,当B抛出异常时,
如果A处理了异常,此时事务A提交。否则,事务A回滚。

3.NESTED底层逻辑是JDBC的savepoint。父事务类似于一个逻辑事务,savepoint将各方法分割了若干物理事务。
4.在嵌套事务中使用事务同步器时需要特别小心。


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

赞同 (28) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽