關于Spring的@Transaction導致數據庫回滾全部生效問題(又刪庫跑路)
很多需要使用事務的場景,都只是在方法上直接添加個@Transactional注解
但是,你以為這真的夠了嗎?
事務如果未達到完美效果,在開發和測試階段都難以被發現,因為你難以考慮到太多意外場景。但當業務數據量發展,就可能導致大量數據不一致的問題,就會造成前人栽樹后人踩坑,需要大量人力排查解決問題和修復數據。
2 如何確認Spring事務生效了?使用@Transactional一鍵開啟聲明式事務, 這就真的事務生效了?過于信任框架總有“意外驚喜”。來看如下案例
領域層 實體
領域服務
createUserError1調用private方法
createUserPrivate,被@Transactional注解。當傳入的用戶名包含test則拋異常,讓用戶的創建操作失敗
getUserCount
用戶接口層
調用UserService#createUserError1
測試結果即便用戶名不合法,用戶也能創建成功。刷新瀏覽器,多次發現有十幾個的非法用戶注冊。 @Transactional生效原則 public方法
除非特殊配置(比如使用AspectJ靜態織入實現AOP),@Transactional必須定義在public方法才生效。
因為Spring的AOP,private方法無法被代理到,自然也無法動態增強事務處理邏輯。
那簡單,把createUserPrivate方法改為public不就行了。但發現事務依舊未生效。
必須通過代理過的類從外部調用目標方法
要調用增強過的方法必然是調用代理后的對象。嘗試修改UserService,注入一個self,然后再通過self實例調用標記有 @Transactional 注解的createUserPublic方法。設置斷點可以看到,self是由Spring通過CGLIB方式增強過的類:
CGLIB通過繼承實現代理類,private方法在子類不可見,所以無法進行事務增強。而this指針代表調用對象本身,Spring不可能注入this,所以通過this訪問方法必然不是代理。把this改為self,這時即可驗證事務生效:非法的用戶注冊操作可回滾。
雖然在UserDomainService內部注入自己調用自己的createUserPublic可正確實現事務,但這不符常規。更合理的實現方式是,讓Controller直接調用之前定義的UserService的createUserPublic方法。
this/self/Controller調用UserDomainService
無法走到Spring代理類
后兩種調用的Spring注入的UserService,通過代理調用才有機會對createUserPublic方法進行動態增強。
推薦開發時打開Debug日志以了解Spring事務實現的細節。比如JPA數據庫訪問,開啟Debug日志:logging.level.org.springframework.orm.jpa=DEBUG
開啟日志后再比較下在UserService中this調用、Controller中通過注入的UserService Bean調用createUserPublic的區別。
很明顯,this調用因沒走代理,事務沒有在createUserPublic生效,只在Repository的save生效:
// 在UserService中通過this調用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調用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
這種實現在Controller里處理異常顯得繁瑣,還不如直接把createUserWrong2加@Transactional注解,然后在Controller中直接調用該方法。這既能從外部(Controller中)調用UserService方法,方法又是public的能夠被動態代理AOP增強。
小結
務必確認調用被@Transactional注解標記的方法被public修飾,并且是通過Spring注入的Bean進行調用。
但有時因沒有正確處理異常,導致事務即便生效也不一定能回滾。
2 事務生效不代表能正確回滾AOP實現事務:使用try/catch包裹@Transactional注解的方法:
當方法出現異常并滿足一定條件,在catch里可設置事務回滾 沒有異常則直接提交事務 一定條件只有異常傳播出了被@Transactional注解的方法,事務才能回滾。
Spring的 TransactionAspectSupport#invokeWithinTransaction 就是在處理事務。觀察源碼得知,只有捕獲到異常后才能進行后續事務處理:
默認情況下,出現RuntimeException(非受檢異常)或Error,Spring才會回滾事務。
Spring的DefaultTransactionAttribute:
受檢異常一般是業務異常或類似另一種方法的返回值,出現這種異常可能業務還能完成,所以不會主動回滾 而Error或RuntimeException代表非預期結果,應該回滾
事務無法正常回滾的各種慘案 異常無法傳播出方法
受檢異常
注冊的同時會有一次文件讀,若讀文件失敗,希望用戶注冊的DB操作回滾。因讀文件拋的是受檢異常,createUserError2傳播出去的也是受檢異常
以上方法雖然避開了事務不生效的坑,但因異常處理不當,導致異常時依舊不回滾事務。
修復回滾失敗bug 1 手動設置讓當前事務處回滾態
若希望自己捕獲異常并處理,可手動設置讓當前事務處回滾態
查看日志,事務確定回滾。
Transactional code has requested rollback:手動請求回滾。
2 注解中聲明,期望所有Exception都回滾事務 突破默認不回滾受檢異常的限制
查看日志,提示回滾:
該案例有DB操作、IO操作,在IO操作問題時期望DB事務也回滾,以確保邏輯一致性。 小結
由于異常處理不正確,導致雖然事務生效,但出現異常時沒回滾。Spring默認只對被@Transactional注解的方法出現RuntimeException和Error時回滾,所以若方法捕獲了異常,就需要通過手寫代碼處理事務回滾。若希望Spring針對其他異常也可回滾,可相應配置@Transactional注解的rollbackFor和noRollbackFor屬性覆蓋Spring的默認配置。
有些業務可能包含多次DB操作,不一定希望將兩次操作作為一個事務,這時就需仔細考慮事務傳播的配置。
3 事務傳播配置是否符合業務邏輯案例
用戶注冊:會插入一個主用戶到用戶表,還會注冊一個關聯的子用戶。期望將子用戶注冊的DB操作作為一個獨立事務,即使失敗也不影響注冊主用戶的流程。
UserService:創建主、子用戶
SubUserService:使子用戶注冊失敗。期望子用戶注冊作為一個事務單獨回滾而不影響注冊主用戶
啟動調用后查看日志:事務回滾了
不對呀!因為運行時異常逃出被@Transactional注解的createUserWrong,Spring當然會回滾事務。若期望主方法不回滾,應捕獲子方法所拋的異常。
修正方案
把subUserService#createSubUserWithExceptionError包上catch,這樣外層主方法createUserError2就不會出現異常
啟動后查看日志注意到:
對createUserError2開啟異常處理 子方法因出現運行時異常,標記當前事務為回滾 主方法捕獲異常并打印create sub user error 主方法提交事務但Controller出現一個UnexpectedRollbackException,異常描述提示最終該事務回滾了且為靜默回滾:因createUserError2本身并無異常,只不過提交后發現子方法已把當前事務設為回滾,無法完成提交。
明明無異常發生,但事務也不一定可提交因為主方法注冊主用戶的邏輯和子方法注冊子用戶的邏輯為同一事務,子邏輯標記了事務需回滾,主邏輯自然也無法提交。那么修復方式就明確了,獨立子邏輯的事務,即修正SubUserService注冊子用戶方法,為注解添加propagation = Propagation.REQUIRES_NEW設置REQUIRES_NEW事務傳播策略。即執行到該方法時開啟新事務,并掛起當前事務。創建一個新事務,若存在則暫停當前事務。類似同名的EJB事務屬性。注:實際事務暫停不會對所有事務管理器外的開箱。 這特別適于org.springframework.transaction.jta.JtaTransactionManager ,這就需要javax.transaction.TransactionManager被提供給它(這是服務器特定的標準Java EE)
主方法無變化,依舊需捕獲異常,防止異常外泄導致主事務回滾,重命名為createUserRight:
修正后再查看日志
Creating new transaction with name createUserRight
對createUserRight開啟主方法事務createMainUser finish創建主用戶完成Suspending current transaction, creating new transaction with name createSubUserWithExceptionRight主事務掛起,開啟新事務,即對createSubUserWithExceptionRight創建子用戶的邏輯Initiating transaction rollback子方法事務回滾Resuming suspended transaction after completion of inner transaction子方法事務完成,繼續主方法之前掛起的事務create sub user error:invalid status主方法捕獲到了子方法的異常Committing JPA transaction on EntityManager主方法的事務提交了,隨后我們在Controller里沒看到靜默回滾異常
小結
若方法涉及多次DB操作,并希望將它們作為獨立事務進行提交或回滾,即需考慮細化配置事務傳播方式,即配置@Transactional注解的Propagation屬性。
4 總結若要針對private方法啟用事務,動態代理方式的AOP不可行,需要使用靜態織入方式的AOP,也就是在編譯期間織入事務增強代碼,可以配置Spring框架使用AspectJ來實現AOP。
以上就是關于Spring的@Transaction導致數據庫回滾全部生效問題(又刪庫跑路)的詳細內容,更多關于Spring @Transaction數據庫回滾的資料請關注好吧啦網其它相關文章!
相關文章:
