關於Spring的@Transaction導致數據庫回滾全部生效問題(又刪庫跑路)

1 前言

很多需要使用事務的場景,都隻是在方法上直接添加個@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

  • this自調用

無法走到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註解的方法出現RuntimeExceptionError時回滾,所以若方法捕獲瞭異常,就需要通過手寫代碼處理事務回滾。
若希望Spring針對其他異常也可回滾,可相應配置@Transactional註解的rollbackFornoRollbackFor屬性覆蓋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數據庫回滾的資料請關註WalkonNet其它相關文章!

推薦閱讀: