Java的@Transactional、@Aysnc、事务同步问题详解

 更新时间:2023年11月27日 08:47:00   作者:ps酷教程  
这篇文章主要介绍了Java的@Transactional、@Aysnc、事务同步问题详解,现在我们需要在一个业务方法中插入一个用户,这个业务方法我们需要加上事务,然后插入用户后,我们要异步的方式打印出数据库中所有存在的用户,需要的朋友可以参考下

场景

我们要做的事情很简单:

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

最初版本

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

TestController

@RestController
@RequestMapping("test")
public class TestController {
    @Autowired
    private TestService testService;
    @GetMapping("testTx")
    public String testTx() {
        testService.doTx();
        return "ok";
    }
}

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-----------------");

    }

}

问题

我们访问上面的这个接口://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

报错版本: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-----------------");

    }

}

@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-----------------");
    }
}

我们继续访问上面的这个接口://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-----------------");
    }
}

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

在这里插入图片描述

到此这篇关于Java的@Transactional、@Aysnc、事务同步问题详解的文章就介绍到这了,更多相关@Transactional、@Aysnc、事务同步问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Sentinel实现动态配置的集群流控的方法

    Sentinel实现动态配置的集群流控的方法

    这篇文章主要介绍了Sentinel实现动态配置的集群流控,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • 关于QueryWrapper高级使用示例

    关于QueryWrapper高级使用示例

    本文介绍了QueryWrapper的高级使用方法,包括查询指定字段、使用MySQL函数处理字段、设置查询限制等,通过select()可查询指定字段并处理,last()方法实现limit效果,apply()可在查询条件中使用函数,这些技巧有助于提升数据库操作的灵活性和效率
    2024-09-09
  • eclipse修改jvm参数调优方法(2种)

    eclipse修改jvm参数调优方法(2种)

    本篇文章主要介绍了eclipse修改jvm参数调优方法(2种),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • 解决Shiro 处理ajax请求拦截登录超时的问题

    解决Shiro 处理ajax请求拦截登录超时的问题

    这篇文章主要介绍了解决Shiro 处理ajax请求拦截登录超时的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • java实现一个桌球小游戏

    java实现一个桌球小游戏

    这篇文章主要为大家详细介绍了java实现一个桌球小游戏,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-07-07
  • SpringBoot接口恶意刷新和暴力请求的解决方法

    SpringBoot接口恶意刷新和暴力请求的解决方法

    在实际项目使用中,必须要考虑服务的安全性,当服务部署到互联网以后,就要考虑服务被恶意请求和暴力攻击的情况,所以本文给大家介绍了SpringBoot接口恶意刷新和暴力请求的解决方法,需要的朋友可以参考下
    2024-11-11
  • 从零搭建脚手架之集成Spring Retry实现失败重试和熔断器模式(实战教程)

    从零搭建脚手架之集成Spring Retry实现失败重试和熔断器模式(实战教程)

    在我们的大多数项目中,会有一些场景需要重试操作,而不是立即失败,让系统更加健壮且不易发生故障,这篇文章主要介绍了从零搭建开发脚手架之集成Spring Retry实现失败重试和熔断器模式,需要的朋友可以参考下
    2022-07-07
  • 对SpringBoot项目Jar包进行加密防止反编译的方案

    对SpringBoot项目Jar包进行加密防止反编译的方案

    最近项目要求部署到其他公司的服务器上,但是又不想将源码泄露出去,要求对正式环境的启动包进行安全性处理,防止客户直接通过反编译工具将代码反编译出来,本文介绍了如何对SpringBoot项目Jar包进行加密防止反编译,需要的朋友可以参考下
    2024-08-08
  • Java实现PDF转图片的三种方法

    Java实现PDF转图片的三种方法

    有些时候我们需要在项目中展示PDF,所以我们可以将PDF转为图片,然后已图片的方式展示,效果很好,Java使用各种技术将pdf转换成图片格式,并且内容不失帧,本文给大家介绍了三种方法实现PDF转图片的案例,需要的朋友可以参考下
    2023-10-10
  • SpringBoot2.0集成MQTT消息推送功能实现

    SpringBoot2.0集成MQTT消息推送功能实现

    这篇文章主要介绍了SpringBoot2.0集成MQTT消息推送功能实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04

最新评论