美文网首页程序员Java 杂谈
被标记为事务的方法互相调用的坑(上)

被标记为事务的方法互相调用的坑(上)

作者: CoderBear | 来源:发表于2018-12-04 15:20 被阅读66次

    相信大家一定用过Spring中的注解型事务,配合上Spring Boot,只需要在方法上打一个@Transactional 就可以完成,真香。

    但是如果大家对其中的机制一知半解的话,可能一不小心就会掉进坑,然后久久无法爬出来。

    下面我就分享下 被标记为事务的方法互相调用的坑

    首先我写两个事务方法:

        @Autowired
        AccountMapper mapper;
    
        @Transactional
        @Override
        public void insertCodeBear() {
            Account account = new Account();
            account.setAccount("CodeBear");
            account.setPassword("CodeBear");
            mapper.insert(account);
        }
    
        @Transactional
        @Override
        public void insertCodeMonkey() {
            Account account = new Account();
            account.setAccount("CodeMonkey");
            account.setPassword("CodeMonkey");
            mapper.insert(account);
        }
    

    现在我想在insertCodeBear方法里面调用insertCodeMonkey方法,但是insertCodeMonkey不是很重要,就算失败,也不能影响到insertCodeBear方法的执行,但是insertCodeMonkey该回滚的还是要回滚,我们很容易写出如下代码:

        @Autowired
        AccountMapper mapper;
    
        @Transactional
        @Override
        public void insertCodeBear() {
            try {
               insertCodeMonkey();
            } catch (Exception ex) {
            }
            Account account = new Account();
            account.setAccount("CodeBear");
            account.setPassword("CodeBear");
            mapper.insert(account);
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        @Override
        public void insertCodeMonkey() {
            Account account = new Account();
            account.setAccount("CodeMonkey");
            account.setPassword("CodeMonkey");
            mapper.insert(account);
            int a = 1 / 0;//自杀代码,便于测试
        }
    

    在第二个方法中,用了自杀代码,便于测试。

    看上去一点问题都没有:第一个方法会成功,第二个方法会失败并且回滚。但是仅仅是看上去,当我们运行一下,会发现奇怪的事情发生了:


    image.png

    两个方法竟然都成功了!!Why?

    为了排查问题,需要开启一下 有关事务 的日志,在 配置文件 中加上下面的配置:

    logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=debug
    

    然后运行,看下控制台打印的内容:


    image.png

    图片可能有点模糊,大家可以在新标签页中打开这图片,可以看到这里分明只开了一个事务,而且事务的传播行为是PROPAGATION_REQUIRED,这是事务的默认传播行为,也就是这里只开启了insertCodeBear方法的事务,并没有开启insertCodeMonkey的事务。

    这是什么原因?为了更好的说明问题产生的原因,我需要手写一个AOP。

    在此之前大家要达成一个共识,@Transactional 其实也是通过AOP去实现的。

    image.png

    AOP有几种实现方式,我这里采用JDK动态代理的方式:

    代码入口:

    public class Main {
        public static void main(String[] args) {
            BookServiceImpl impl = new BookServiceImpl();
            InvocationHandler myInvocationHandler = new MyInvocationHandler(impl);
            Object o = Proxy.newProxyInstance(myInvocationHandler.getClass().getClassLoader(),
                    impl.getClass().getInterfaces(), myInvocationHandler);
            ((IBookService) o).add();
        }
    }
    
    

    接口:

    public interface IBookService {
        void add();
    
        void delete();
    }
    

    实现类:

    public class BookServiceImpl implements IBookService {
        public void add() {
            delete();
            System.out.println("add");
        }
    
        public void delete() {
            System.out.println("delete");
        }
    }
    

    切面定义:

    public class MyInvocationHandler implements InvocationHandler {
        private Object obj;
    
        public MyInvocationHandler(Object obj) {
            this.obj = obj;
        }
    
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("开始啦,小伙子");
            method.invoke(obj, args);
            System.out.println("结束啦,小伙子");
            return null;
        }
    }
    

    在Main入口里面调用了实现类的代理对象,调用了add方法,add方法里面又调用了delete的方法。很简单吧。按照我们的想法,应该是打印出两次 切面中定义的话,但是事实是 只打印了一次:


    image.png

    让我们在切面方法中加上这行代码:

     System.out.println("方法是" + method.getName());
    

    看看是哪个方法进入到了这里。

    运行:


    image.png

    add方法进入到了这里,但是delete方法却没有进来。

    让我们再回到第一个例子,为了让大家看的清楚一点,我再贴上insertCodeBear被调用的代码:

    @RestController
    @RequestMapping("/CodeBear")
    public class HelloWorldController {
        @Autowired
        AccountService service;
    
        @GetMapping("/insert")
        public void insert() {
            service.insertCodeBear();
        }
    }
    

    AccountService 是一个接口,里面定义了insertCodeBear和insertCodeMonkey虚方法。
    我们打一个断点在

     service.insertCodeBear();
    

    这里,然后调试看下service是一个什么东西:


    image.png

    你会发现,service已经不是简单的AccountService 的实现类了,而是实现类的代理对象,从这里也可以看出,其实@Transactional也是通过AOP去实现的。

    通过两个例子,可以得到一个结论:只有调用代理对象的方法才能被拦截,所以 在方法A中直接调用方法B,方法B是不会被拦截的

    这也就是为什么insertCodeMonkey的事务没有被开启的原因了,因为insertCodeMonkey方法是insertCodeBear直接调用的。

    那么,这个问题该如何解决呢?在下一篇博客,我会采用几种方式来解决这个问题(这篇博客已经比较长了,因为加上了很多看上去没什么用的“废话”,因为可以直接写出结论,然后再写解决方案就是了。但是我还是很详细的,把“废话”都写出来了,就是因为分析问题的思路才是最重要的 )。

    相关文章

      网友评论

      本文标题:被标记为事务的方法互相调用的坑(上)

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