分析一个Jdk动态代理和@Transcational带来的一个bug

431 阅读2分钟

开端

之前在面试的时候,被面试官问到了JDK动态代理和Transaction可能会带来什么问题,当时脑子里突然懵了,不知道面试官想说什么,再细问的时候,面试官就一带而过了,不过现在想想面试官也是在考研我们的实战经验,做了一番查询后发现,在某些场景下可能会有事务失效的问题。

一、问题重现

我们先上例子

例子一:

    @Override
    @Transactional
    public void save1() {
        Account account = new Account();
        account.setName("zhangsan");
        account.setMoney(100f);

        dao.insert(account);
        save2();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save2() {
        Account account = new Account();
        account.setName("lisi");
        account.setMoney(200f);
        
        dao.insert(account);
    }

现在有两个方法,save1()和save2(),在某些业务中,我们需要save1()调用save2(),但是save2方法执行失败的话,不能影响到save1(),这个时候我们使用Propagation.REQUIRES_NEW来重开一个事务,测试代码如下

    @Test
    public void save1() {
        accountService.save1();
    }

执行save1()后,数据库正常保存。 ,然后我们在save2()显示的抛出一个异常,代码如下

例子二:

    @Override
    @Transactional
    public void save1() {
        Account account = new Account();
        account.setName("zhangsan");
        account.setMoney(100f);

        dao.insert(account);
        save2();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save2() {
        Account account = new Account();
        account.setName("lisi");
        account.setMoney(200f);
        
        dao.insert(account);
        System.out.println(1/0);
    }

这个时候程序会报错,但是我们看下数据库save1()的方法也没有执行成功,这是为什么呢,正常来说我们已经在save2()重新开了一个事务,save2()报错不应该影响save1()才对啊,这个时候想到了可能是save2()方法报错并没有catch住,导致save1()方法也报错了,我们再改下代码

例子三:

    @Override
    @Transactional
    public void save1() {
        Account account = new Account();
        account.setName("zhangsan");
        account.setMoney(100f);

        dao.insert(account);

        save2();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save2() {
        Account account = new Account();
        account.setName("lisi");
        account.setMoney(200f);

        dao.insert(account);
        try {

            System.out.println(1/0);
        }catch (ArithmeticException e){
            System.out.println("不能除以零");
        }
    }

这个时候执行完成发现两条数据都插进了数据库,这又是怎么回事?完全不按照套路出牌啊,我是谁,我从哪里来,我要到哪里去....

二、分析问题本质

我们都知道Spring事务管理用的是动态代理的方式实现的,而动态代理有jdk动态代理和cglib动态代理两种,默认情况下且被代理类是接口的实现类的情况下,spring使用的是jdk代理。

当我们使用在调用save方法时,spring使用的是代理类来给管理我们的事务,如果我们使用spring注入的方式自然是没有问题的,可是如果在本类直接调用的话,并不是代理类调用,自然也就不会走代理了,save2()方法配置的事务也就不生效了,其实例子三相当于如下代码

例子四:

    @Override
    @Transactional
    public void save1() {
        Account account = new Account();
        account.setName("zhangsan");
        account.setMoney(100f);

        dao.insert(account);

        Account account = new Account();
        account.setName("lisi");
        account.setMoney(200f);

        dao.insert(account);
        try {

            System.out.println(1/0);
        }catch (ArithmeticException e){
            System.out.println("不能除以零");
        }
    }

所以他们是在同一个事务中,不管怎么操作,都是同时提交或者同时回滚。