關(guān)于Spring的@Transaction導(dǎo)致數(shù)據(jù)庫回滾全部生效問題(又刪庫跑路)
1 前言
很多需要使用事務(wù)的場景,都只是在方法上直接添加個@Transactional注解

但是,你以為這真的夠了嗎?
事務(wù)如果未達到完美效果,在開發(fā)和測試階段都難以被發(fā)現(xiàn),因為你難以考慮到太多意外場景。但當(dāng)業(yè)務(wù)數(shù)據(jù)量發(fā)展,就可能導(dǎo)致大量數(shù)據(jù)不一致的問題,就會造成前人栽樹后人踩坑,需要大量人力排查解決問題和修復(fù)數(shù)據(jù)。
2 如何確認(rèn)Spring事務(wù)生效了?
使用@Transactional一鍵開啟聲明式事務(wù), 這就真的事務(wù)生效了?過于信任框架總有“意外驚喜”。來看如下案例
領(lǐng)域?qū)?實體


領(lǐng)域服務(wù)
createUserError1調(diào)用private方法

createUserPrivate,被@Transactional注解。當(dāng)傳入的用戶名包含test則拋異常,讓用戶的創(chuàng)建操作失敗

getUserCount

用戶接口層
調(diào)用UserService#createUserError1

測試結(jié)果
即便用戶名不合法,用戶也能創(chuàng)建成功。刷新瀏覽器,多次發(fā)現(xiàn)有十幾個的非法用戶注冊。 @Transactional生效原則 public方法
除非特殊配置(比如使用AspectJ靜態(tài)織入實現(xiàn)AOP),@Transactional必須定義在public方法才生效。
因為Spring的AOP,private方法無法被代理到,自然也無法動態(tài)增強事務(wù)處理邏輯。
那簡單,把createUserPrivate方法改為public不就行了。
但發(fā)現(xiàn)事務(wù)依舊未生效。
必須通過代理過的類從外部調(diào)用目標(biāo)方法
要調(diào)用增強過的方法必然是調(diào)用代理后的對象。
嘗試修改UserService,注入一個self,然后再通過self實例調(diào)用標(biāo)記有 @Transactional 注解的createUserPublic方法。設(shè)置斷點可以看到,self是由Spring通過CGLIB方式增強過的類:

CGLIB通過繼承實現(xiàn)代理類,private方法在子類不可見,所以無法進行事務(wù)增強。而this指針代表調(diào)用對象本身,Spring不可能注入this,所以通過this訪問方法必然不是代理。
把this改為self,這時即可驗證事務(wù)生效:非法的用戶注冊操作可回滾。
雖然在UserDomainService內(nèi)部注入自己調(diào)用自己的createUserPublic可正確實現(xiàn)事務(wù),但這不符常規(guī)。更合理的實現(xiàn)方式是,讓Controller直接調(diào)用之前定義的UserService的createUserPublic方法。

this/self/Controller調(diào)用UserDomainService

- this自調(diào)用
無法走到Spring代理類
- 后兩種
調(diào)用的Spring注入的UserService,通過代理調(diào)用才有機會對createUserPublic方法進行動態(tài)增強。
推薦開發(fā)時打開Debug日志以了解Spring事務(wù)實現(xiàn)的細(xì)節(jié)。
比如JPA數(shù)據(jù)庫訪問,開啟Debug日志:
logging.level.org.springframework.orm.jpa=DEBUG
開啟日志后再比較下在UserService中this調(diào)用、Controller中通過注入的UserService Bean調(diào)用createUserPublic的區(qū)別。
很明顯,this調(diào)用因沒走代理,事務(wù)沒有在createUserPublic生效,只在Repository的save生效:
// 在UserService中通過this調(diào)用public的createUserPublic [23:04:30.748] [http-nio-45678-exec-5] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:370 ] - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT //在Controller中通過注入的UserService Bean調(diào)用createUserPublic [10:10:47.750] [http-nio-45678-exec-6] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo1.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
這種實現(xiàn)在Controller里處理異常顯得繁瑣,還不如直接把createUserWrong2加@Transactional注解,然后在Controller中直接調(diào)用該方法。
這既能從外部(Controller中)調(diào)用UserService方法,方法又是public的能夠被動態(tài)代理AOP增強。
小結(jié)
務(wù)必確認(rèn)調(diào)用被@Transactional注解標(biāo)記的方法被public修飾,并且是通過Spring注入的Bean進行調(diào)用。
但有時因沒有正確處理異常,導(dǎo)致事務(wù)即便生效也不一定能回滾。
2 事務(wù)生效不代表能正確回滾
AOP實現(xiàn)事務(wù):使用try/catch包裹@Transactional注解的方法:
- 當(dāng)方法出現(xiàn)異常并滿足一定條件,在catch里可設(shè)置事務(wù)回滾
- 沒有異常則直接提交事務(wù) 一定條件
只有異常傳播出了被@Transactional注解的方法,事務(wù)才能回滾。
Spring的 TransactionAspectSupport#invokeWithinTransaction 就是在處理事務(wù)。觀察源碼得知,只有捕獲到異常后才能進行后續(xù)事務(wù)處理:

默認(rèn)情況下,出現(xiàn)RuntimeException(非受檢異常)或Error,Spring才會回滾事務(wù)。
Spring的DefaultTransactionAttribute:
- 受檢異常一般是業(yè)務(wù)異?;蝾愃屏硪环N方法的返回值,出現(xiàn)這種異常可能業(yè)務(wù)還能完成,所以不會主動回滾
- 而Error或RuntimeException代表非預(yù)期結(jié)果,應(yīng)該回滾
事務(wù)無法正?;貪L的各種慘案 異常無法傳播出方法

受檢異常
注冊的同時會有一次文件讀,若讀文件失敗,希望用戶注冊的DB操作回滾。因讀文件拋的是受檢異常,createUserError2傳播出去的也是受檢異常


以上方法雖然避開了事務(wù)不生效的坑,但因異常處理不當(dāng),導(dǎo)致異常時依舊不回滾事務(wù)。
修復(fù)回滾失敗bug 1 手動設(shè)置讓當(dāng)前事務(wù)處回滾態(tài)
若希望自己捕獲異常并處理,可手動設(shè)置讓當(dāng)前事務(wù)處回滾態(tài)

查看日志,事務(wù)確定回滾。
Transactional code has requested rollback:手動請求回滾。
2 注解中聲明,期望所有Exception都回滾事務(wù) 突破默認(rèn)不回滾受檢異常的限制

查看日志,提示回滾:

該案例有DB操作、IO操作,在IO操作問題時期望DB事務(wù)也回滾,以確保邏輯一致性。 小結(jié)
由于異常處理不正確,導(dǎo)致雖然事務(wù)生效,但出現(xiàn)異常時沒回滾。
Spring默認(rèn)只對被@Transactional注解的方法出現(xiàn)RuntimeException和Error時回滾,所以若方法捕獲了異常,就需要通過手寫代碼處理事務(wù)回滾。
若希望Spring針對其他異常也可回滾,可相應(yīng)配置@Transactional注解的rollbackFor和noRollbackFor屬性覆蓋Spring的默認(rèn)配置。
有些業(yè)務(wù)可能包含多次DB操作,不一定希望將兩次操作作為一個事務(wù),這時就需仔細(xì)考慮事務(wù)傳播的配置。
3 事務(wù)傳播配置是否符合業(yè)務(wù)邏輯
案例
用戶注冊:會插入一個主用戶到用戶表,還會注冊一個關(guān)聯(lián)的子用戶。期望將子用戶注冊的DB操作作為一個獨立事務(wù),即使失敗也不影響注冊主用戶的流程。
UserService:創(chuàng)建主、子用戶

SubUserService:使子用戶注冊失敗。期望子用戶注冊作為一個事務(wù)單獨回滾而不影響注冊主用戶

啟動調(diào)用后查看日志:事務(wù)回滾了

不對呀!因為運行時異常逃出被@Transactional注解的createUserWrong,Spring當(dāng)然會回滾事務(wù)。若期望主方法不回滾,應(yīng)捕獲子方法所拋的異常。
修正方案
把subUserService#createSubUserWithExceptionError包上catch,這樣外層主方法createUserError2就不會出現(xiàn)異常

啟動后查看日志注意到:
- 對
createUserError2開啟異常處理 - 子方法因出現(xiàn)運行時異常,標(biāo)記當(dāng)前事務(wù)為回滾
- 主方法捕獲異常并打印
create sub user error - 主方法提交事務(wù)
但Controller出現(xiàn)一個UnexpectedRollbackException,異常描述提示最終該事務(wù)回滾了且為靜默回滾:因createUserError2本身并無異常,只不過提交后發(fā)現(xiàn)子方法已把當(dāng)前事務(wù)設(shè)為回滾,無法完成提交。
明明無異常發(fā)生,但事務(wù)也不一定可提交
因為主方法注冊主用戶的邏輯和子方法注冊子用戶的邏輯為同一事務(wù),子邏輯標(biāo)記了事務(wù)需回滾,主邏輯自然也無法提交。
那么修復(fù)方式就明確了,獨立子邏輯的事務(wù),即修正SubUserService注冊子用戶方法,為注解添加propagation = Propagation.REQUIRES_NEW設(shè)置REQUIRES_NEW事務(wù)傳播策略。即執(zhí)行到該方法時開啟新事務(wù),并掛起當(dāng)前事務(wù)。
創(chuàng)建一個新事務(wù),若存在則暫停當(dāng)前事務(wù)。類似同名的EJB事務(wù)屬性。
注:實際事務(wù)暫停不會對所有事務(wù)管理器外的開箱。 這特別適于org.springframework.transaction.jta.JtaTransactionManager ,這就需要javax.transaction.TransactionManager被提供給它(這是服務(wù)器特定的標(biāo)準(zhǔn)Java EE)

主方法無變化,依舊需捕獲異常,防止異常外泄導(dǎo)致主事務(wù)回滾,重命名為createUserRight:

修正后再查看日志
Creating new transaction with name createUserRight
對createUserRight開啟主方法事務(wù)
createMainUser finish
創(chuàng)建主用戶完成
Suspending current transaction, creating new transaction with name createSubUserWithExceptionRight
主事務(wù)掛起,開啟新事務(wù),即對createSubUserWithExceptionRight創(chuàng)建子用戶的邏輯
Initiating transaction rollback
子方法事務(wù)回滾
Resuming suspended transaction after completion of inner transaction
子方法事務(wù)完成,繼續(xù)主方法之前掛起的事務(wù)
create sub user error:invalid status
主方法捕獲到了子方法的異常
Committing JPA transaction on EntityManager
主方法的事務(wù)提交了,隨后我們在Controller里沒看到靜默回滾異常
小結(jié)
若方法涉及多次DB操作,并希望將它們作為獨立事務(wù)進行提交或回滾,即需考慮細(xì)化配置事務(wù)傳播方式,即配置@Transactional注解的Propagation屬性。
4 總結(jié)
若要針對private方法啟用事務(wù),動態(tài)代理方式的AOP不可行,需要使用靜態(tài)織入方式的AOP,也就是在編譯期間織入事務(wù)增強代碼,可以配置Spring框架使用AspectJ來實現(xiàn)AOP。
以上就是關(guān)于Spring的@Transaction導(dǎo)致數(shù)據(jù)庫回滾全部生效問題(又刪庫跑路)的詳細(xì)內(nèi)容,更多關(guān)于Spring @Transaction數(shù)據(jù)庫回滾的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?Boot?實現(xiàn)字段唯一校驗功能(實例代碼)
這篇文章主要介紹了Spring?Boot?實現(xiàn)字段唯一校驗,實現(xiàn)代碼很簡單,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08
springbooot使用google驗證碼的功能實現(xiàn)
這篇文章主要介紹了springbooot使用google驗證碼,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05
SpringBoot+MyBatis-Plus實現(xiàn)分頁的項目實踐
MyBatis-Plus是基于MyBatis的持久層增強工具,提供簡化CRUD、代碼生成器、條件構(gòu)造器、分頁及樂觀鎖等功能,極大簡化了開發(fā)工作量并提高了開發(fā)效率,本文就來介紹一下SpringBoot+MyBatis-Plus實現(xiàn)分頁的項目實踐,感興趣的可以了解一下2024-11-11
Spring事件發(fā)布監(jiān)聽,順序監(jiān)聽,異步監(jiān)聽方式
這篇文章主要介紹了Spring事件發(fā)布監(jiān)聽,順序監(jiān)聽,異步監(jiān)聽方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12



