• @Transactional & @Aysnc & 循环依赖 & 事务同步问题


    学习链接

    @Async学习及循环依赖

    场景

    我们要做的事情很简单:

    1. 现在我们需要在一个业务方法中插入一个用户,
    2. 这个业务方法我们需要加上事务,
    3. 然后插入用户后,我们要异步的方式打印出数据库中所有存在的用户。

    最初版本

    我们的代码在最开始,可能是如下:

    TestController
    @RestController
    @RequestMapping("test")
    public class TestController {
    
        @Autowired
        private TestService testService;
        
        @GetMapping("testTx")
        public String testTx() {
    
            testService.doTx();
    
            return "ok";
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    TestService
    @Slf4j
    @Service
    @EnableAsync // 开启异步
    @EnableTransactionManagement // 开启事务
    public class TestService {
    
        @Autowired
        private UserService userService;
    
        @Transactional
        public void doTx(){
            log.info("-----------------doTx-----------------" + this.getClass());
    
            User user = new User();
            user.setNickname(RandomStringUtils.randomAlphabetic(5));
    
            userService.save(user); // 插入用户
            log.info("插入用户:{}" , user);
    
            printUserList(); // 我们希望的是异步打印所有的用户
    
            log.info("-----------------doTx-----------------");
    
            try {
                Thread.sleep(3000); // 这里还需要干其它的活,反正就是这里不确定,万一它就卡在这里了呢, 就模拟这个情况
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void printUserList() {
            log.info("-----------------printUserList-----------------" + this.getClass());
            List<User> list = userService.list(new QueryWrapper<User>());
            for (User user1 : list) {
                log.info("printUser:  {}",user1);
            }
            log.info("-----------------printUserList-----------------");
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    问题

    我们访问上面的这个接口:http://localhost:8085/web-api/test/testTx,输出如下的日志。

    发现问题:可以看到 保存用户 和 异步打印所有用户 用的是同一个线程,说好的异步没有了,为什么没有异步了呢?可以看到我们使用的仍然是TestService而不是代理对象,所以直接就是调用的就是TestService类的方法,而异步注解是基于代理的(但不是基于自动代理创建器的),所以就有问题了。

    在这里插入图片描述

    @Lazy版本 + 事务同步

    既然,上面我们知道了,是由于没有调用代理,所以异步打印所有用户仍然用的是原来的线程。那么再问一句:TestService没有被代理吗?它的的确确被代理了,是因为@Transactional让它做了事务代理,但是事务代理基于的就是aop,aop责任链调用的最终节点,调用的是真实对象,所以那里就用的是真实对象去打印,那可不就没代理了嘛!

    原因,我们也知道了,那我们可以让它自己注入自己,发现启动报错,启动报错的原因在于@Async实现代理的方式 和 aop的自动代理方式 用的不是同一个代理创建器。在一般情况下,自己注入自己的确是可以解决这种循环依赖 + 自动代理的问题的(或者用AopContxt.currentProxy()获取到绑定到当前线程的代理对象),但是一旦碰到这种@Async 和 aop自动代理的情况,由于有2个代理创建器存在,且它们都要对这个对象进行代理,那就有问题了。会报如下的错误:

    Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService': Bean with name 'testService' has been injected into other beans [testService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
    	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622)
    	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
    	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
    	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1251)
    	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1171)
    	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:593)
    	... 19 common frames omitted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    报错版本:TestService
    @Slf4j
    @Service
    @EnableAsync // 开启异步
    @EnableTransactionManagement // 开启事务
    public class TestService {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private TestService testService;
    
        @Transactional
        public void doTx(){
            log.info("-----------------doTx-----------------" + this.getClass());
    
            User user = new User();
            user.setNickname(RandomStringUtils.randomAlphabetic(5));
    
            userService.save(user); // 插入用户
            log.info("插入用户:{}" , user);
    
            testService.printUserList(); // 我们希望的是异步打印所有的用户
    
            log.info("-----------------doTx-----------------");
    
            try {
                Thread.sleep(3000); // 这里还需要干其它的活,反正就是这里不确定,万一它就卡在这里了呢, 就模拟这个情况
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void printUserList() {
            log.info("-----------------printUserList-----------------" + this.getClass());
            List<User> list = userService.list(new QueryWrapper<User>());
            for (User user1 : list) {
                log.info("printUser:  {}",user1);
            }
            log.info("-----------------printUserList-----------------");
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    @Lazy正常启动版本(有问题)

    给TestService加个@Lazy注解,就可以解决这个问题,解决的方式是因为在解析含有@Lazy注解的依赖时,会创建一个代理对象,这个代理把从spring容器中获取目标bean的时机,调整到了使用它的时候,也就是说,往TestService中注入的testService,在解析依赖的解决,不去容器中去找或者创建,而是直接构建了个代理对象,放入到里面。这样就相当于没有发生循环发生一样,因为循环依赖产生的的时机就是在解析bean的依赖的时候,通过@Lazy创建代理的方式处理了依赖,也就不存在这个循环依赖的问题了。

    也好比说:我在TestService中注入一个容器中压根就没有定义的bean,但是我给这个这个字段上的bean加了@Lazy注解,它依然可以正常启动,当然,在用的时候,它仍然会报错。但在这里没关系,在启动阶段已经不报错了,在运行阶段,会去容器中寻找testService,而在运行阶段,spring容器已经初始化好了,也就没问题了。

    @Slf4j
    @Service
    @EnableAsync // 开启异步
    @EnableTransactionManagement // 开启事务
    public class TestService {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        @Lazy
        private TestService testService;
    
        @Transactional
        public void doTx(){
            log.info("-----------------doTx-----------------" + this.getClass());
    
            User user = new User();
            user.setNickname(RandomStringUtils.randomAlphabetic(5));
    
            userService.save(user); // 插入用户
            log.info("插入用户:{}" , user);
    
            testService.printUserList(); // 我们希望的是异步打印所有的用户
    
            log.info("-----------------doTx-----------------");
    
            try {
                Thread.sleep(3000); // 这里还需要干其它的活,反正就是这里不确定,万一它就卡在这里了呢, 就模拟这个情况
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void printUserList() {
            log.info("-----------------printUserList-----------------" + this.getClass());
            List<User> list = userService.list(new QueryWrapper<User>());
            for (User user1 : list) {
                log.info("printUser:  {}",user1);
            }
            log.info("-----------------printUserList-----------------");
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    我们继续访问上面的这个接口:http://localhost:8085/web-api/test/testTx,输出如下的日志。

    异步打印的问题是解决了,但是,又有个问题了,查出来怎么只会有1个用户呢?这个接口调用了2次,肯定会有2个用户的,现在却只有一个用户,原因就在于是异步打印的,当前事务还有提交,然后就去查询,肯定就只会查询1个出来

    在这里插入图片描述

    @Lazy + 注册事务同步

    上面代码中,调用@Aysnc注解修饰的异步方法应该是要在事务提交了之后,再去调用,而不是插入数据之后调用!所以需要注册事务同步到事务同步管理器中,在事务提交之后,再去作异步任务,这样异步任务才能在数据库中查到刚刚插入的数据。感觉有点像vue里面的nextTick了。

    @Slf4j
    @Service
    @EnableAsync // 开启异步
    @EnableTransactionManagement // 开启事务
    public class TestService {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        @Lazy
        private TestService testService;
    
        @Transactional
        public void doTx(){
            log.info("-----------------doTx-----------------" + this.getClass());
    
            User user = new User();
            user.setNickname(RandomStringUtils.randomAlphabetic(5));
    
            userService.save(user); // 插入用户
            log.info("插入用户:{}" , user);
    
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    testService.printUserList();// 我们希望的是异步打印所有的用户
                }
            });
    
            log.info("-----------------doTx-----------------");
    
            try {
                Thread.sleep(3000); // 这里还需要干其它的活,反正就是这里不确定,万一它就卡在这里了呢, 就模拟这个情况
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Async
        public void printUserList() {
            log.info("-----------------printUserList-----------------" + this.getClass());
            List<User> list = userService.list(new QueryWrapper<User>());
            for (User user1 : list) {
                log.info("printUser:  {}",user1);
            }
            log.info("-----------------printUserList-----------------");
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    可以看到,刚刚插入的时id为4用户,现在能够把刚刚插入的查询出来了

    在这里插入图片描述

  • 相关阅读:
    Java_只出现一次的数字
    Windows网络监视工具
    Android的自启动
    Spring Bean 的作用域(Bean Scope)
    y125.第七章 服务网格与治理-Istio从入门到精通 -- Sidecar及流量拦截机制和访问网格外部服务(十一)
    毕业从事弱电3个月,我为什么会选择转行网络工程师
    PyCharm+PyQT5之三界面与逻辑的分离
    egg框架使用(一)
    【Django框架】——22 Django视图 04 HttpRequest对象
    TiDB x 安能物流丨打造一栈式物流数据平台
  • 原文地址:https://blog.csdn.net/qq_16992475/article/details/130870840