盤點(diǎn)Spring事務(wù)失效的4種寫法及解決方案,Review代碼再也不慌了
1、非運(yùn)行時(shí)異常導(dǎo)致事務(wù)無法回滾
我們知道,Spring是通過AOP的方式來實(shí)現(xiàn)事務(wù)的,而在處理事務(wù)的過程中,Spring只有捕獲到RuntimeException或者Error的時(shí)候才會(huì)觸發(fā)回滾操作,如果我們?cè)诖a中拋出的是非運(yùn)行時(shí)異常,而又沒有特殊配置的話,事務(wù)就會(huì)無法回滾。
下面我們以一個(gè)簡(jiǎn)單的例子,復(fù)現(xiàn)一下這種情況,以及針對(duì)這種情況的解決方案。
本文Springboot版本:2.7.6,數(shù)據(jù)源為MySQL。
首先創(chuàng)建一個(gè)測(cè)試用的User對(duì)象:
建表語(yǔ)句:
測(cè)試邏輯:往user表插入一條數(shù)據(jù),如果插入成功,就拋出exception異常,測(cè)試數(shù)據(jù)是否回滾。
新建測(cè)試方法:
運(yùn)行測(cè)試方法,從控制臺(tái)可以看到,我們手動(dòng)指定的異常被成功拋出。
但是,當(dāng)異常發(fā)生時(shí),事務(wù)并沒有被回滾,數(shù)據(jù)依然被插入到了數(shù)據(jù)庫(kù)。
解決辦法:
1,將異常包裝成運(yùn)行時(shí)異常:throw new RuntimeException("異?;貪L測(cè)試");
2,在@Transactional指定回滾的異常類型,@Transactional(rollbackFor = Exception.class)。
一般來說,使用第二種方式會(huì)更清晰一些,但是有些朋友往往會(huì)忘記手動(dòng)指定回滾的異常類型,進(jìn)而導(dǎo)致非預(yù)期的bug產(chǎn)生。
2、通過this調(diào)用本類事務(wù)方法導(dǎo)致的事務(wù)無法回滾
隨著業(yè)務(wù)的發(fā)展,核心業(yè)務(wù)代碼會(huì)越來越多,同一個(gè)方法也會(huì)越寫越長(zhǎng)。我們?yōu)榱耸勾a邏輯更加高內(nèi)聚低耦合,會(huì)將功能相同的代碼進(jìn)行封裝成一個(gè)個(gè)的子方法。
但是,如果我們對(duì)事務(wù)的運(yùn)行機(jī)制了解不透徹,隨意在同一個(gè)類中通過this調(diào)用事務(wù)方法,就可能導(dǎo)致非預(yù)期的bug。
如以上代碼所示,在addUser方法中調(diào)用了事務(wù)方法doAddUser,如果數(shù)據(jù)插入成功,就拋出一個(gè)異常,測(cè)試數(shù)據(jù)是否能夠回滾。
通過測(cè)試用例可以看到,異常已經(jīng)拋出,但是數(shù)據(jù)庫(kù)中卻成功的插入了數(shù)據(jù),我們期望的數(shù)據(jù)并沒有回滾。
原因探究:
原因其實(shí)很簡(jiǎn)單,通過this方法調(diào)用時(shí),Spring的代理沒能起作用,事務(wù)自然也就無法介入,關(guān)于這一點(diǎn)的原理在之前的文章中也有分析過,感興趣的朋友可以去看一看。
有的朋友可能會(huì)說,項(xiàng)目的代碼已經(jīng)是這樣了,再將老方法重寫到新類中也不現(xiàn)實(shí),有沒有辦法改動(dòng)較小的方式呢?
其實(shí)很簡(jiǎn)單,現(xiàn)在事務(wù)失效的原因是代理失效,那么想辦法讓代理重新生效就行了。
我們?cè)诒绢愔凶⑷胍粋€(gè)當(dāng)前對(duì)象,這個(gè)對(duì)象可以被Spring代理,那么這個(gè)對(duì)象的方法自然也可以被代理。
3、被聲明的事務(wù)方法是private類型
這種錯(cuò)誤在博主剛工作時(shí)遇到挺多次的,不過現(xiàn)在現(xiàn)代IDE已經(jīng)越來越智能了,對(duì)于這種情況會(huì)直接給出錯(cuò)誤提示,所以這里提出這種錯(cuò)誤只是告訴大家,事務(wù)方法是不能聲明為private的。
至于為什么不能是private,那自然還是和代理有關(guān)了。
4、嵌套事務(wù)異常導(dǎo)致事務(wù)被提前關(guān)閉而報(bào)錯(cuò)
當(dāng)使用嵌套事務(wù)時(shí),需要明確指定事務(wù)的傳播范圍。
如以上代碼,我們添加完一條數(shù)據(jù)之后,嘗試將密碼更新為666666,并且希望即使更新異常,也不要影響添加操作。
然而運(yùn)行測(cè)試用例,我們會(huì)得到這樣一條錯(cuò)誤信息:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。
什么意思呢?就是當(dāng)Spring處理事務(wù)時(shí),發(fā)現(xiàn)事務(wù)已經(jīng)被回滾了。
這是因?yàn)槲覀儾]有指定事務(wù)的傳播行為,默認(rèn)情況下,Spring的事務(wù)傳播是REQUIRED,即:如果本來有事務(wù),則加入該事務(wù),如果沒有事務(wù),則創(chuàng)建新的事務(wù)。
我們添加數(shù)據(jù)時(shí)啟動(dòng)了一個(gè)事務(wù),更新數(shù)據(jù)時(shí),Spring判斷當(dāng)前已經(jīng)存在事務(wù),所以就不再新建事務(wù),而是加入當(dāng)前事務(wù)。
但是當(dāng)更新操作失敗時(shí),需要對(duì)事務(wù)進(jìn)行回滾,更新是沒問題的,正常回滾。
但是插入操作就不行了,當(dāng)要提交插入操作的事務(wù)時(shí),由于事務(wù)已經(jīng)被回滾了,無法再次操作,Spring只好報(bào)錯(cuò)來提示我們了。
如何處理呢?在更新操作上指明事務(wù)的傳播范圍就行。
再測(cè)試一下,發(fā)現(xiàn)插入操作的事務(wù)可以正常提交了。
總結(jié)
事務(wù)是我們?nèi)粘i_發(fā)工作中無法避免的一個(gè)功能,深刻理解事務(wù)的運(yùn)行機(jī)制,正確使用事務(wù)的聲明式操作,才能讓我們寫出更健壯的代碼。