Java的@Transactional、@Aysnc、事務(wù)同步問(wèn)題詳解
場(chǎng)景
我們要做的事情很簡(jiǎn)單:
- 現(xiàn)在我們需要在一個(gè)業(yè)務(wù)方法中插入一個(gè)用戶,
- 這個(gè)業(yè)務(wù)方法我們需要加上事務(wù),
- 然后插入用戶后,我們要異步的方式打印出數(shù)據(jù)庫(kù)中所有存在的用戶。
最初版本
我們的代碼在最開(kāi)始,可能是如下:
TestController
@RestController @RequestMapping("test") public class TestController { @Autowired private TestService testService; @GetMapping("testTx") public String testTx() { testService.doTx(); return "ok"; } }
TestService
@Slf4j @Service @EnableAsync // 開(kāi)啟異步 @EnableTransactionManagement // 開(kāi)啟事務(wù) 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); // 這里還需要干其它的活,反正就是這里不確定,萬(wàn)一它就卡在這里了呢, 就模擬這個(gè)情況 } 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-----------------"); } }
問(wèn)題
我們?cè)L問(wèn)上面的這個(gè)接口://localhost:8085/web-api/test/testTx,輸出如下的日志。
發(fā)現(xiàn)問(wèn)題:可以看到 保存用戶 和 異步打印所有用戶 用的是同一個(gè)線程,說(shuō)好的異步?jīng)]有了,為什么沒(méi)有異步了呢?可以看到我們使用的仍然是TestService而不是代理對(duì)象,所以直接就是調(diào)用的就是TestService類的方法,而異步注解是基于代理的(但不是基于自動(dòng)代理創(chuàng)建器的),所以就有問(wèn)題了。
@Lazy版本 + 事務(wù)同步
既然,上面我們知道了,是由于沒(méi)有調(diào)用代理,所以異步打印所有用戶仍然用的是原來(lái)的線程。那么再問(wèn)一句:TestService沒(méi)有被代理嗎?它的的確確被代理了,是因?yàn)锧Transactional讓它做了事務(wù)代理,但是事務(wù)代理基于的就是aop,aop責(zé)任鏈調(diào)用的最終節(jié)點(diǎn),調(diào)用的是真實(shí)對(duì)象,所以那里就用的是真實(shí)對(duì)象去打印,那可不就沒(méi)代理了嘛!
原因,我們也知道了,那我們可以讓它自己注入自己,發(fā)現(xiàn)啟動(dòng)報(bào)錯(cuò),啟動(dòng)報(bào)錯(cuò)的原因在于@Async實(shí)現(xiàn)代理的方式 和 aop的自動(dòng)代理方式 用的不是同一個(gè)代理創(chuàng)建器。在一般情況下,自己注入自己的確是可以解決這種循環(huán)依賴 + 自動(dòng)代理的問(wèn)題的(或者用AopContxt.currentProxy()獲取到綁定到當(dāng)前線程的代理對(duì)象),但是一旦碰到這種@Async 和 aop自動(dòng)代理的情況,由于有2個(gè)代理創(chuàng)建器存在,且它們都要對(duì)這個(gè)對(duì)象進(jìn)行代理,那就有問(wèn)題了。會(huì)報(bào)如下的錯(cuò)誤:
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
報(bào)錯(cuò)版本:TestService
@Slf4j @Service @EnableAsync // 開(kāi)啟異步 @EnableTransactionManagement // 開(kāi)啟事務(wù) 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); // 這里還需要干其它的活,反正就是這里不確定,萬(wàn)一它就卡在這里了呢, 就模擬這個(gè)情況 } 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正常啟動(dòng)版本(有問(wèn)題)
給TestService加個(gè)@Lazy注解,就可以解決這個(gè)問(wèn)題,解決的方式是因?yàn)樵诮馕龊蠤Lazy注解的依賴時(shí),會(huì)創(chuàng)建一個(gè)代理對(duì)象,這個(gè)代理把從spring容器中獲取目標(biāo)bean的時(shí)機(jī),調(diào)整到了使用它的時(shí)候,也就是說(shuō),往TestService中注入的testService,在解析依賴的解決,不去容器中去找或者創(chuàng)建,而是直接構(gòu)建了個(gè)代理對(duì)象,放入到里面。這樣就相當(dāng)于沒(méi)有發(fā)生循環(huán)發(fā)生一樣
,因?yàn)檠h(huán)依賴產(chǎn)生的的時(shí)機(jī)就是在解析bean的依賴的時(shí)候,通過(guò)@Lazy創(chuàng)建代理的方式處理了依賴,也就不存在這個(gè)循環(huán)依賴的問(wèn)題了。
也好比說(shuō):我在TestService中注入一個(gè)容器中壓根就沒(méi)有定義的bean,但是我給這個(gè)這個(gè)字段上的bean加了@Lazy注解,它依然可以正常啟動(dòng),當(dāng)然,在用的時(shí)候,它仍然會(huì)報(bào)錯(cuò)。但在這里沒(méi)關(guān)系,在啟動(dòng)階段已經(jīng)不報(bào)錯(cuò)了,在運(yùn)行階段,會(huì)去容器中尋找testService,而在運(yùn)行階段,spring容器已經(jīng)初始化好了,也就沒(méi)問(wèn)題了。
@Slf4j @Service @EnableAsync // 開(kāi)啟異步 @EnableTransactionManagement // 開(kāi)啟事務(wù) 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); // 這里還需要干其它的活,反正就是這里不確定,萬(wàn)一它就卡在這里了呢, 就模擬這個(gè)情況 } 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-----------------"); } }
我們繼續(xù)訪問(wèn)上面的這個(gè)接口://localhost:8085/web-api/test/testTx,輸出如下的日志。
異步打印的問(wèn)題是解決了,但是,又有個(gè)問(wèn)題了,查出來(lái)怎么只會(huì)有1個(gè)用戶呢?這個(gè)接口調(diào)用了2次,肯定會(huì)有2個(gè)用戶的,現(xiàn)在卻只有一個(gè)用戶,原因就在于是異步打印的,當(dāng)前事務(wù)還有提交,然后就去查詢,肯定就只會(huì)查詢1個(gè)出來(lái)。
@Lazy + 注冊(cè)事務(wù)同步
上面代碼中,調(diào)用@Aysnc注解修飾的異步方法應(yīng)該是要在事務(wù)提交了之后,再去調(diào)用,而不是插入數(shù)據(jù)之后調(diào)用!
所以需要注冊(cè)事務(wù)同步到事務(wù)同步管理器中,在事務(wù)提交之后,再去作異步任務(wù),這樣異步任務(wù)才能在數(shù)據(jù)庫(kù)中查到剛剛插入的數(shù)據(jù)。感覺(jué)有點(diǎn)像vue里面的nextTick了。
@Slf4j @Service @EnableAsync // 開(kāi)啟異步 @EnableTransactionManagement // 開(kāi)啟事務(wù) 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); // 這里還需要干其它的活,反正就是這里不確定,萬(wàn)一它就卡在這里了呢, 就模擬這個(gè)情況 } 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-----------------"); } }
可以看到,剛剛插入的時(shí)id為4用戶,現(xiàn)在能夠把剛剛插入的查詢出來(lái)了
到此這篇關(guān)于Java的@Transactional、@Aysnc、事務(wù)同步問(wèn)題詳解的文章就介紹到這了,更多相關(guān)@Transactional、@Aysnc、事務(wù)同步問(wèn)題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Sentinel實(shí)現(xiàn)動(dòng)態(tài)配置的集群流控的方法
這篇文章主要介紹了Sentinel實(shí)現(xiàn)動(dòng)態(tài)配置的集群流控,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04關(guān)于QueryWrapper高級(jí)使用示例
本文介紹了QueryWrapper的高級(jí)使用方法,包括查詢指定字段、使用MySQL函數(shù)處理字段、設(shè)置查詢限制等,通過(guò)select()可查詢指定字段并處理,last()方法實(shí)現(xiàn)limit效果,apply()可在查詢條件中使用函數(shù),這些技巧有助于提升數(shù)據(jù)庫(kù)操作的靈活性和效率2024-09-09eclipse修改jvm參數(shù)調(diào)優(yōu)方法(2種)
本篇文章主要介紹了eclipse修改jvm參數(shù)調(diào)優(yōu)方法(2種),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02解決Shiro 處理ajax請(qǐng)求攔截登錄超時(shí)的問(wèn)題
這篇文章主要介紹了解決Shiro 處理ajax請(qǐng)求攔截登錄超時(shí)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot接口惡意刷新和暴力請(qǐng)求的解決方法
在實(shí)際項(xiàng)目使用中,必須要考慮服務(wù)的安全性,當(dāng)服務(wù)部署到互聯(lián)網(wǎng)以后,就要考慮服務(wù)被惡意請(qǐng)求和暴力攻擊的情況,所以本文給大家介紹了SpringBoot接口惡意刷新和暴力請(qǐng)求的解決方法,需要的朋友可以參考下2024-11-11從零搭建腳手架之集成Spring?Retry實(shí)現(xiàn)失敗重試和熔斷器模式(實(shí)戰(zhàn)教程)
在我們的大多數(shù)項(xiàng)目中,會(huì)有一些場(chǎng)景需要重試操作,而不是立即失敗,讓系統(tǒng)更加健壯且不易發(fā)生故障,這篇文章主要介紹了從零搭建開(kāi)發(fā)腳手架之集成Spring?Retry實(shí)現(xiàn)失敗重試和熔斷器模式,需要的朋友可以參考下2022-07-07對(duì)SpringBoot項(xiàng)目Jar包進(jìn)行加密防止反編譯的方案
最近項(xiàng)目要求部署到其他公司的服務(wù)器上,但是又不想將源碼泄露出去,要求對(duì)正式環(huán)境的啟動(dòng)包進(jìn)行安全性處理,防止客戶直接通過(guò)反編譯工具將代碼反編譯出來(lái),本文介紹了如何對(duì)SpringBoot項(xiàng)目Jar包進(jìn)行加密防止反編譯,需要的朋友可以參考下2024-08-08Java實(shí)現(xiàn)PDF轉(zhuǎn)圖片的三種方法
有些時(shí)候我們需要在項(xiàng)目中展示PDF,所以我們可以將PDF轉(zhuǎn)為圖片,然后已圖片的方式展示,效果很好,Java使用各種技術(shù)將pdf轉(zhuǎn)換成圖片格式,并且內(nèi)容不失幀,本文給大家介紹了三種方法實(shí)現(xiàn)PDF轉(zhuǎn)圖片的案例,需要的朋友可以參考下2023-10-10SpringBoot2.0集成MQTT消息推送功能實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot2.0集成MQTT消息推送功能實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04