美文网首页Beautiful Java@IT·互联网程序员
玩转Spring —— 消失的事务

玩转Spring —— 消失的事务

作者: 柳树之 | 来源:发表于2018-06-20 08:20 被阅读630次

    消失的事务

    端午节前,组内在讨论一个问题:

    一个没有加@Transactional注解的方法,去调用一个加了@Transactional的方法,会不会产生事务?

    文字苍白,还是用代码说话。

    先写一个@Transactional的方法(本文的所有代码,可到Github上下载):

    @Transactional
    public void deleteAllAndAddOneTransactional(Customer customer) {
      customerRepository.deleteAll();
      if ("Yang".equals(customer.getFirstName())) {
        throw new RuntimeException();
      }
      customerRepository.save(customer);
    }
    

    方法内先去执行deleteAll(),删除表中全部数据;然后执行save()保存数据。

    这两个方法中间,会判断传进来的firstName是不是等于“Yang”,是,则抛异常,用于模拟两个数据库操作之间可能发生的异常场景。

    如果没有加@Transactional注解,那么这两个操作就不在一个事务里面,不具有原子性。如果deleteAll之后抛异常,那么就会导致只删除不新增。

    而加了@Transactional之后,这两个动作在一个事务里头,具有原子性,要么全部成功,要么全部失败。如果deleteAll之后抛异常,则事务回滚,恢复原先被删除的数据。

    测试一下,启动Spring Boot工程,首先调用findAll接口,看看数据库中都有哪些数据:

    接着调用deleteAndSave接口,故意传入firstName="Yang",果然返回失败:

    然后,在回过头去调用findAll接口,看看数据是不是还在:


    数据都在,说明产生事务了。

    上面都没啥,都跟符合我们的直觉。

    问题来了,如果我的接口是去调用一个没有加@Transactional的方法,然后这个方法再去调用加了@Transactional的方法呢?

        public void deleteAllAndAddOne(Customer customer) {
            System.out.println("go into deleteAllAndAddOne");
            deleteAllAndAddOneTransactional(customer);
        }
    
    
        @Transactional
        public void deleteAllAndAddOneTransactional(Customer customer) {
            customerRepository.deleteAll();
            if ("Yang".equals(customer.getFirstName())) {
                throw new RuntimeException();
            }
            customerRepository.save(customer);
        }
    

    直觉告诉我,会的。

    重新编译,启动,调用新的接口,继续故意让它抛异常:

    然后再去findAll,看看数据还在不在:

    WTF! 空空如也!数据都没了!

    看来我又一次被直觉欺骗了。

    还是得老老实实看代码,弄懂原理。

    看了一晚上代码,恍然大悟。咱们先画个图解释一下,再来看看代码。

    图解@Transactional

    首先,我们得先弄懂@Transactional的原理。

    为什么第一种情况,也就是直接调用@Transactional方法,会产生事务?

    其实Spring的@Transactional,跟Spring AOP一样,都是利用了动态代理。

    我们写了一个类,里面写了一个加了@Transactional注解的方法,这原本平淡无奇,什么用也没有,就像这样:

    关键在于,Spring在检查到@Transactional注解之后,给这个对象生成了一个代理对象proxy:

    代理对象的methodB,会先开启事务(beginTransaction),然后再去执行原先对象target的methodB,如果抛异常,则回滚(rollBack),如果一切顺利,则提交(commit)。

    而最后注入Spring容器的,也正是这个带有事务逻辑的代理对象。所以我们调用methodB时会产生事务。

    现在,我们写了一个新方法,methodA,里头去调用methodB:

    从上面的分析,可以知道,我们最终拿到的,是代理对象。

    那么代理对象的methodA是长什么样子的呢?长这样:

    由于methodA没有加@Transactional注解,所以代理对象里面,直接就是target.methodA(),直接调用了原来对象的methodA。

    这下就很清晰了,代理对象的methodA,去调用原来对象的methodA,原来对象的methodA,再去调用原来对象的methodB,而原来对象的methodB,是不具有事务的。事务只存在于代理对象的methodB. 所以整个方法也就没有事务了。

    看看代码

    最后再来看看代码。

    只需要在deleteAllAndAddOneTransactional方法内,打一个断点,一切了然。

    分别调用两个接口,比较调用堆栈:

    明显可以看出,直接调用@Transactional方法,堆栈更长,而且会经过一个叫TransactionInterceptor的拦截器。

    跟着堆栈往上走,会发现关键就在于这个if-else的逻辑,CglibAopProxy:

    CglibAopProxy会去检查要调用的方法,有没有AOP调用链:

    • 没有,则走if里面的逻辑,直接调用target对象的方法,也就是上面间接调用@Transactional方法的情形;
    • 有,则走else逻辑,也就是直接调用@Transactional方法的情形。

    当然,如果deleteAllAndAddOne方法被别的切面拦截,那么调用链chain也不会为空,也会走if逻辑,这时候是否会有事务呢?思考题。

    上面贴的代码是Cglib代理的情形,JDK Proxy的,大家自行欣赏。

    总结

    这篇文章主要讲了一个有点违反直觉的现象。

    通过这样一个例子,希望能够加深大家对Spring动态代理的理解。

    现在想想,面试时,为什么那么喜欢问源码?

    其实道理很简单,你用了这项技术、这个框架,却不知道它是怎么实现的,那就有可能造成错误的使用。

    参考

    • 《Spring揭秘》

    相关文章

      网友评论

      • 君子不器124:图好评
      • 黄老斜:对象自己调用自己的方法当然没事务了,缓存也是一样,所有通过切面实现的功能,都没效果
      • 384d48bce2f4:这个问题怎么解决呢?我得想法是在methodA加上Transactional注解,代理类调用原methodA再调用原methodB,当methodB向上抛出异常到代理类,再回滚。不知这样是否可行?
        唯一浩哥:@TYBK_4834 很简单,只需要把当前类的实例注入进来,用其调用即可
        0ff9d0b3244c:@TYBK_4834 事务传播机制会帮助你解决这个问题
        柳树之:@TYBK_4834 可以是可以 但是这样就强制要求其他调用methodB的方法也要加上注解,不是很友好。解决方法有:1、把methodB放到另一个Service里,不要在service内部调用;2、使用编程式事务,不使用注解;3、打开expose-target开关(具体怎么操作可以搜一下)
      • d776106b3acb:学习了
      • 994b403cd3b7:图很萌嘿嘿
      • a0c270905aaf:有些疑惑,请教下,methodA方法 怎么产生动态代理对象methodA对象的。为什么methodA方法要产生代理对象,不是很懂,请帮忙解惑,不胜感激。
        柳树之:@vicoqi 代理的主体是对象,不是方法。
        a0c270905aaf:@SexyCode 还有点儿疑问,methodA方法怎么会在 pointcut 内,不是应该加了@Transactional 注解的方法,才会被生成代理并被调用吗?
        柳树之:@vicoqi 不是专门给methodA生成代理。而是因为methodB需要加切面,所以spring会给整个对象都会生成一个代理对象。

      本文标题:玩转Spring —— 消失的事务

      本文链接:https://www.haomeiwen.com/subject/yswneftx.html