警惕!3個(gè)Spring事務(wù)的大坑
前言
Spring框架已是JAVA項(xiàng)目的標(biāo)配,其中Spring事務(wù)管理也是最常用的一個(gè)功能,但如果不了解其實(shí)現(xiàn)原理,使用姿勢不對,一不小心就可能掉坑里。
為了更透徹的說明這些坑,本文分四部分展開闡述:第一部分簡單介紹下Spring事務(wù)集成的幾種方式;第二部分結(jié)合Spring源代碼說明Spring事務(wù)的實(shí)現(xiàn)原理;第三部分通過實(shí)際測試代碼介紹關(guān)于Spring事務(wù)的坑;第四部分是對本文的總結(jié)。
一、Spring事務(wù)管理的幾種方式:
Spring事務(wù)在具體使用方式上可分為兩大類:
1. 聲明式
基于 TransactionProxyFactoryBean的聲明式事務(wù)管理
基于 和 命名空間的事務(wù)管理
基于 @Transactional 的聲明式事務(wù)管理
2. 編程式
基于事務(wù)管理器API 的編程式事務(wù)管理
基于TransactionTemplate 的編程式事務(wù)管理
目前大部分項(xiàng)目使用的是聲明式的后兩種:
基于 和 命名空間的聲明式事務(wù)管理可以充分利用切點(diǎn)表達(dá)式的強(qiáng)大支持,使得管理事務(wù)更加靈活。 基于 @Transactional 的方式需要實(shí)施事務(wù)管理的方法或者類上使用 @Transactional 指定事務(wù)規(guī)則即可實(shí)現(xiàn)事務(wù)管理,在Spring Boot中通常也建議使用這種注解方式來標(biāo)記事務(wù)。
二、Spring事務(wù)實(shí)現(xiàn)機(jī)制
接下來我們詳細(xì)看下Spring事務(wù)的源代碼,進(jìn)而了解其工作原理。我們從標(biāo)簽的解析類開始:


由此可看到Spring事務(wù)的核心實(shí)現(xiàn)類TransactionInterceptor及其父類TransactionAspectSupport,其實(shí)現(xiàn)了事務(wù)的開啟、數(shù)據(jù)庫操作、事務(wù)提交、回滾等。我們平時(shí)在開發(fā)時(shí)如果想確定是否在事務(wù)中,也可以在該方法進(jìn)行斷點(diǎn)調(diào)試。
TransactionInterceptor:

TransactionAspectSupport

至此我們了解事務(wù)的整個(gè)調(diào)用流程,但還有一個(gè)重要的機(jī)制沒分析到,那就是Spring 事務(wù)針對不同的傳播級別控制當(dāng)前獲取的數(shù)據(jù)庫連接。接下來我們看下Spring獲取連接的工具類DataSourceUtils,JdbcTemplate、Mybatis-Spring也都是通過該類獲取Connection。

TransactionSynchronizationManager也是一個(gè)事務(wù)同步管理的核心類,它實(shí)現(xiàn)了事務(wù)同步管理的職能,包括記錄當(dāng)前連接持有connection holder。
TransactionSynchronizationManager

在事務(wù)管理器類AbstractPlatformTransactionManager中,getTransaction獲取事務(wù)時(shí),會(huì)處理不同的事務(wù)傳播行為,例如當(dāng)前存在事務(wù),但調(diào)用方法事務(wù)傳播級別為REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED時(shí),對當(dāng)前事務(wù)進(jìn)行掛起、恢復(fù)等操作,以此保證了當(dāng)前數(shù)據(jù)庫操作獲取正確的Connection。
具體是在子事務(wù)提交的最后會(huì)將掛起的事務(wù)恢復(fù),恢復(fù)時(shí)重新調(diào)用TransactionSynchronizationManager. bindResource設(shè)置之前的connection holder,這樣再獲取的連接就是被恢復(fù)的數(shù)據(jù)庫連接, TransactionSynchronizationManager當(dāng)前激活的連接只能是一個(gè)。
AbstractPlatformTransactionManager

Spring的事務(wù)是通過AOP代理類中的一個(gè)Advice(TransactionInterceptor)進(jìn)行生效的,而傳播級別定義了事務(wù)與子事務(wù)獲取連接、事務(wù)提交、回滾的具體方式。
AOP(Aspect Oriented Programming),即面向切面編程。Spring AOP技術(shù)實(shí)現(xiàn)上其實(shí)就是代理類,具體可分為靜態(tài)代理和動(dòng)態(tài)代理兩大類,其中靜態(tài)代理是指使用 AOP 框架提供的命令進(jìn)行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時(shí)增強(qiáng);(AspectJ);而動(dòng)態(tài)代理則在運(yùn)行時(shí)借助于 默寫類庫在內(nèi)存中“臨時(shí)”生成 AOP 動(dòng)態(tài)代理類,因此也被稱為運(yùn)行時(shí)增強(qiáng)。其中java是使用的動(dòng)態(tài)代理模式 (JDK+CGLIB)。
JDK動(dòng)態(tài)代理 JDK動(dòng)態(tài)代理主要涉及到j(luò)ava.lang.reflect包中的兩個(gè)類:Proxy和InvocationHandler。InvocationHandler是一個(gè)接口,通過實(shí)現(xiàn)該接口定義橫切邏輯,并通過反射機(jī)制調(diào)用目標(biāo)類的代碼,動(dòng)態(tài)將橫切邏輯和業(yè)務(wù)邏輯編制在一起。Proxy利用InvocationHandler動(dòng)態(tài)創(chuàng)建一個(gè)符合某一接口的實(shí)例,生成目標(biāo)類的代理對象。
CGLIB動(dòng)態(tài)代理 CGLIB全稱為Code Generation Library,是一個(gè)強(qiáng)大的高性能,高質(zhì)量的代碼生成類庫,可以在運(yùn)行期擴(kuò)展Java類與實(shí)現(xiàn)Java接口,CGLIB封裝了asm,可以再運(yùn)行期動(dòng)態(tài)生成新的class。和JDK動(dòng)態(tài)代理相比較:JDK創(chuàng)建代理有一個(gè)限制,就是只能為接口創(chuàng)建代理實(shí)例,而對于沒有通過接口定義業(yè)務(wù)方法的類,則可以通過CGLIB創(chuàng)建動(dòng)態(tài)代理。
CGLIB 創(chuàng)建代理的速度比較慢,但創(chuàng)建代理后運(yùn)行的速度卻非???,而 JDK 動(dòng)態(tài)代理正好相反。如果在運(yùn)行的時(shí)候不斷地用 CGLIB 去創(chuàng)建代理,系統(tǒng)的性能會(huì)大打折扣。因此如果有接口,Spring默認(rèn)使用JDK 動(dòng)態(tài)代理,源代碼如下:

在了解Spring代理的兩種特點(diǎn)后,我們也就知道在做事務(wù)切面配置時(shí)的一些注意事項(xiàng),例如JDK代理時(shí)方法必須是public,CGLIB代理時(shí)必須是public、protected,且類不能是final的;在依賴注入時(shí),如果屬性類型定義為實(shí)現(xiàn)類,JDK代理時(shí)會(huì)報(bào)如下注入異常:

但如果修改為CGLIB代理時(shí)則會(huì)成功注入,所以如果有接口,建議注入時(shí)該類屬性都定義為接口。另外事務(wù)切點(diǎn)都配置在實(shí)現(xiàn)類和接口都可以生效,但建議加在實(shí)現(xiàn)類上。
官網(wǎng)關(guān)于Spring AOP的詳細(xì)介紹
docs.spring.io/spring/docs…
三、Spring事務(wù)的那些坑
通過之前章節(jié),相信您已經(jīng)掌握了spring事務(wù)的使用方式與原理,不過還是要注意,因?yàn)橐徊恍⌒木涂赡芫偷艨?。首先看第一個(gè)坑:
3.1 事務(wù)不生效
測試代碼,事務(wù)AOP配置:



1.運(yùn)行輸出:

2.運(yùn)行輸出:

3.運(yùn)行輸出:
insertAccount connection hashcode=303240439 insertStock connection hashcode=303240439可以看到2、3測試方法跟我們事務(wù)預(yù)期并一樣,結(jié)論:調(diào)用方法未配置事務(wù)、本類方法直接調(diào)用,事務(wù)都不生效!
究其原因,還是因?yàn)镾pring的事務(wù)本質(zhì)上是個(gè)代理類,而本類方法直接調(diào)用時(shí)其對象本身并不是織入事務(wù)的代理,所以事務(wù)切面并未生效。具體可以參見#Spring事務(wù)實(shí)現(xiàn)機(jī)制#章節(jié)。
Spring也提供了判斷是否為代理的方法:

那如何修改為代理類調(diào)用呢?最直接的想法是注入自身,代碼如下:

當(dāng)然Spring提供了獲取當(dāng)前代理的方法:代碼如下:

另外Spring是通過TransactionSynchronizationManager類中線程變量來獲取事務(wù)中數(shù)據(jù)庫連接,所以如果是多線程調(diào)用或者繞過Spring獲取數(shù)據(jù)庫連接,都會(huì)導(dǎo)致Spring事務(wù)配置失效。
最后Spring事務(wù)配置失效的場景:
事務(wù)切面未配置正確
本類方法調(diào)用
多線程調(diào)用
繞開Spring獲取數(shù)據(jù)庫連接
接下來我們看下Spring的事務(wù)的另外一個(gè)坑:
3.2 事務(wù)不回滾
測試代碼:


輸出結(jié)果:
insertAccount connection hashcode=656479172 updateAccount connection hashcode=517355658 account balance is 8000.0應(yīng)用拋出異常,但accountDao.updateAccount卻進(jìn)行了提交。究其原因,直接看Spring源代碼:
TransactionAspectSupport

由代碼可見,Spring事務(wù)默認(rèn)只對RuntimeException和Error進(jìn)行回滾,如果應(yīng)用需要對指定的異常類進(jìn)行回滾,可配置rollback-for=屬性,例如:
事務(wù)不回滾的原因:
事務(wù)配置切面未生效
應(yīng)用方法中將異常捕獲
拋出的異常不屬于運(yùn)行時(shí)異常(例如IOException),
rollback-for屬性配置不正確
接下來我們看下Spring事務(wù)的第三個(gè)坑:
3.3 事務(wù)超時(shí)不生效
測試代碼:



正常運(yùn)行,事務(wù)超時(shí)未生效

拋出事務(wù)超時(shí)異常,超時(shí)生效
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Nov 23 17:03:02 CST 2018 at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:141) …
通過源碼看看Spring事務(wù)超時(shí)的判斷機(jī)制:
ResourceHolderSupport

通過查看getTimeToLiveInMillis方法的Call Hierarchy,可以看到被DataSourceUtils的applyTimeout所調(diào)用, 繼續(xù)看applyTimeout的Call Hierarchy,可以看到有兩處調(diào)用,一個(gè)是JdbcTemplate,一個(gè)是TransactionAwareInvocationHandler類,后者是只有TransactionAwareDataSourceProxy類調(diào)用,該類為DataSource的事務(wù)代理類,我們一般并不會(huì)用到。難道超時(shí)只能在這調(diào)用JdbcTemplate中生效?寫代碼親測:
運(yùn)行正常,事務(wù)超時(shí)失效
由上可見:Spring事務(wù)超時(shí)判斷在通過JdbcTemplate的數(shù)據(jù)庫操作時(shí),所以如果超時(shí)后未有JdbcTemplate方法調(diào)用,則無法準(zhǔn)確判斷超時(shí)。另外也可以得知,如果通過Mybatis等操作數(shù)據(jù)庫,Spring的事務(wù)超時(shí)是無效的。鑒于此,Spring的事務(wù)超時(shí)謹(jǐn)慎使用。
四、 總結(jié)
JDBC規(guī)范中Connection 的setAutoCommit是原生控制手動(dòng)事務(wù)的方法,但傳播行為、異常回滾、連接管理等很多技術(shù)問題都需要開發(fā)者自己處理,而Spring事務(wù)通過AOP方式非常優(yōu)雅的屏蔽了這些技術(shù)復(fù)雜度,使得事務(wù)管理變的異常簡單。
但凡事有利弊,如果對實(shí)現(xiàn)機(jī)制理解不透徹,很容易掉坑里。最后總結(jié)下Spring事務(wù)的可能踩的坑:
1. Spring事務(wù)未生效
調(diào)用方法本身未正確配置事務(wù)
本類方法直接調(diào)用
數(shù)據(jù)庫操作未通過Spring的DataSourceUtils獲取Connection
多線程調(diào)用
2. Spring事務(wù)回滾失效
未準(zhǔn)確配置rollback-for屬性
異常類不屬于RuntimeException與Error
應(yīng)用捕獲了異常未拋出
3. Spring事務(wù)超時(shí)不準(zhǔn)確或失效
超時(shí)發(fā)生在最后一次JdbcTemplate操作之后
通過非JdbcTemplate操作數(shù)據(jù)庫,例如Mybatis
了解更多,請點(diǎn)擊:https://www.bilibili.com/video/BV1L7411N77n
作者:你丫才CRUD
鏈接:https://juejin.cn/post/6900794629814550536
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。