聊聊Spring註解@Transactional失效的那些事

2023-07-18 12:01:05

一、前言

emm,又又又踩坑啦。這次的需求主要是對逾期計算的需求任務進行優化,現有的計算任務執行時間太長了。簡單描述下此次的問題:在專案中進行多個資料庫執行操作時,我們期望的是將其整個封裝成一個事務,要麼全部成功,或者全部失敗,然而在自測異常場景時發現,裡面涉及的第一個資料狀態更新成功了,但是後面的資料在插入出現異常,後面查詢資料表發現,該資料的狀態已經被更新成功啦

emmm,檢視程式碼發現確實是使用了@Transactional註解沒問啊。於是通過查詢網上相關資料發現,在使用Spring中事務註解@Transactional時會存在幾種場景下該註解失效,即不能按照預期封裝成一個事務操作,於是對該註解進行學習並對相關失效場景進行分析,整理文章如下;

二、@Transactional註解失效場景範例驗證

1、@Transactional註解屬性

屬性 型別 描述
value String 可選的限定描述符,指定使用的事務管理器
propagation Enum:Propagation· 可選的事務傳播行為設定
isolation Enum:Isolation 可選的事務隔離級別設定
readOnly boolean 讀寫或唯讀事務,預設讀寫
timeout int 事務超時時間設定
rollbackFor Class物件陣列,必須繼承自Throwable 導致事務回滾的異常類陣列
rollbackForClassName 類名陣列,必須繼承自Throwable 導致事務回滾的異常類名字陣列
noRollbackFor Class物件陣列,必須繼承自Throwable 不會導致事務回滾的異常類陣列
noRollbackForClassName 類名陣列,必須繼承自Throwable 不會導致事務回滾的異常類名字陣列

2、 propagation屬性

propagation代表事務的傳播行為,預設值為Propagation.REQUIRED

屬性 描述
Propagation.REQUIRED 若當前存在事務則加入該事務,若不存在則建立一個新事務(預設)
Propagation.SUPPORTS 若當前存在事務則加入該事務,若不存在則以非事務的方式繼續進行
Propagation.MANDATORY 若當前存在事務則加入該事務,若不存在則丟擲異常
Propagation.REQUIRES_NEW 重新建立一個新的事務,若當前存在事務則暫定當前事務
Propagation.NOT_SUPPORTED 以非事務的方式執行,若當前存在事務則暫定當前事務
Propagation.NEVER 以非事務的方式執行,若當前存在事務則丟擲異常
Propagation.NESTED 與Propagation.REQUIRED效果一樣

3、 @Transactional註解使用場景?

@Transactional註解可以作用在介面、類、類方法中。

  • 當作用於類時,表示所有該類的public方法都設定相同的事務屬性資訊。

  • 當作用於方法時,當類設定了@Transactional註解,方法也設定了@Transactional,方法的事務會覆蓋類的事務設定資訊。

  • 當作用於介面時,不推薦使用,因為在介面使用@Transactional並且設定了Spring AOP使用CGLib動態代理將會導致其失效。

4、 @Transactional註解失效場景?

  • @Transactional註解作用在非public修飾的方法上,會失效。

失效原因:在Spring AOP代理時,TransactionInterceptor(事務攔截器)在目標方法執行前後進行攔截,DynamicAdvisedInterceptor(CglibAopProxy的內部類)的Intercept方法或JDKDynamicAopProxyinvoke方法會間接呼叫AbstractFallbackTransationAttributeSourcecomputeTransactionAttribute方法,獲取@Transactional註解的事務設定資訊。

1 protected TransactionAttribute computeTransactionAttribute(Method method,
2    Class<?> targetClass) {
3        // Don't allow no-public methods as required.
4        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
5        return null;
6    }

此方法會檢查目標方法的修飾符是否為public,非public作用域則不會獲取@transactional的屬性設定資訊。其中protected、private修飾的方法上使用 @Transactional註解,事務會失效但不會有任何報錯。

  • @Transactional註解屬性propagation設定錯誤導致註解失效

失效原因:設定錯誤, PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER三種事務傳播方式不會發生回滾。

▪ 範例驗證:寫了一個demo進行測試。demo主要功能如下:執行兩次資料庫插入操作,並在擴充套件資訊欄位中新增備註;

▪ 執行結果如下,構造的單號不存在訂單查詢為空觸發異常,觀察資料庫發現,第一次資料庫插入操作已經執行成功,故而驗證@Transactional註解失效

  • @Transactional註解屬性rollbackFor設定錯誤導致註解失效

rollbackFor可以指定能夠觸發事務回滾的異常型別。Spring預設丟擲了unchecked異常(繼承自RuntimeException)或者Error才會回滾事務。若事務中丟擲了其他型別的異常,但卻期望Spring能夠回滾事務,就需要指定rollbackFor屬性,否則就會失效。

  • 同一類中方法呼叫,導致@Transactional失效

比如類demo中有方法A和B,方法B中使用@Transactional註解,方法A沒有註解,但是demo類通過方法A呼叫方法B,像這種間接呼叫會導致方法B中的@Transactional事務註解失效。

失效原因:只有當事務方法被當前類以外的程式碼呼叫時,才會有Spring生成的代理物件管理。(Spring AOP代理機制造成的)。

範例驗證:demo中構造場景為在同一個類中,在test方法中新增@Transactional註解,querRiskScore方法中不新增該註解,然後在querRiskScore方法中呼叫test方法;觀察下多個插入操作是否會因為異常而中斷回滾;

▪ 執行結果如下,還是通過構造的單號不存在訂單查詢為空觸發異常,觀察資料庫發現,第一次資料庫插入操作已經執行成功第二次資料插入操作失敗,並沒有因為異常而觸發事務操作,故而驗證@Transactional註解方法間的呼叫會失效

  • 多執行緒任務可能導致@Transaction案例失效

失效原因:執行緒不屬於Spring託管,故執行緒不能夠預設使用Spring的事務,也不能獲取Spring注入的bean,在被Spring宣告式事務管理的方法內開啟多執行緒,多執行緒內的方法不被事務控制。

  • 異常被方法內catch捕獲導致@Transactional失效

比如B方法內部拋了異常,而A方法此時try-catch了B方法的異常,則該事務不能正常回滾。

失效原因:因為B方法中丟擲異常以後,標識當前事務需要rollback,但是A方法中由於你手動的捕獲這個異常並進行處理,A方法認為當前事務應該正常commit,此時就出現前後不一致,會丟擲org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only異常。

範例驗證:這個場景的本質還是異常被捕獲導致無法正常的丟擲,進而導致@Transactional註解無法正常工作,我簡化了下demo範例場景,構造場景如下:在querRiskScore方法中新增@Transactional註解,然後在querRiskScore方法中對異常進行捕獲;觀察下多個插入操作是否會因為異常而中斷回滾;

▪ 執行結果如下,還是通過構造的單號不存在訂單查詢為空觸發異常,但是我們在方法內部對該異常進行捕獲,並未向上層丟擲,我們期望的場景是兩次資料插入執行失敗,但是觀察資料庫發現,第一次資料庫插入操作已經執行成功第二次資料插入執行成功,與我們的預期結果不符,故而驗證@Transactional註解在方法中異常被捕獲的場景中失效

究其原因:Spring的事務是在呼叫業務方法之前開始的,業務方法執行完畢之後才執行commit 或 rollback,事務是否執行取決於是否丟擲runtime異常,如果丟擲runtime exception並在你的業務方法中並沒有catch到的話,事務就會回滾。

三、「事務」知識回顧

1.什麼是事務?

事務(Transaction)是由一系列對系統中資料進行存取與更新的操作組成的一個程式執行邏輯單元(Unit)。

通常我們所指的事務是指資料庫事務,使用資料庫事務有以下兩處優點:

  • 當多個應用程式並行存取資料庫時,事務可以在這些應用程式之間提供一個隔離方法,以防止彼此的操作互相干擾

  • 事務為資料庫操作序列提供了一個從失敗恢復到正常狀態的方法,同時提供了資料庫即使在異常狀態下仍能保持資料一致性的方法。

2. 事務具有的特性?

原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性,簡稱事務的ACID特性。

  • 原子性

事務的原子性是指事務必須是一個原子的操作序列單元,即事務中包含的各項操作在一次執行過程中只會出現兩種狀態:全部成功執行、全部不執行。任何一項操作失敗都將導致整個事務失敗,同時其他已經被執行的操作都將被複原並回滾,只打所有的操作全部成功,整個事務才算是成功完成。

  • 一致性

事務的一致性是指事務的執行不能破壞資料庫資料的完整性和一致性,一個事務在執行之前和執行之後,資料庫都必須處於一致性狀態。也就是說,事務執行的結果必須是使資料庫從一個一致性狀態轉變到另一個一致性狀態,因此當資料庫只包含成功事務提交 的結果時,就能說資料庫處於一致性狀態。而如果資料庫系統在執行過程中發生故障, 有些事務尚未完成就被迫中斷,這些未完成的事務對資料庫所做的修改有一部分已寫入物理資料庫,這時資料庫就處於一種不正確的狀態,或者說是不一致的狀態。

  • 隔離性

事務的隔離性是指在並行環境中,並行的事務是相互隔離的,一個事務的執行不能被其他事務干擾。也就是說,不同的事務並行操縱相同的資料時,每個事務都有各自完整的資料空間,即一個事務內部的操作及使用的資料對其他並行事務是隔離的,並行執行的 各個事務之間不能互相干擾。

  • 永續性

事務一旦提交,其所做的修改就會永久儲存到資料庫中,即使資料庫發生故障也不應該對其有任何影響。需要注意的是,事務的永續性不能做到100%的持久,只能從事務本身的角度來保證永久性,而一些外部原因導致資料庫發生故障,如硬碟損壞,那麼所有提交的資料可能都會丟失。

3. 什麼是Spring中的事務?

Spring中同樣提供了很好的事務管理機制,主要分為程式設計式事務宣告式事務

  • 程式設計式事務

是指在程式碼中手動的管理事務的提交、回滾等操作,程式碼侵入性比較強。程式設計式事務方式需要開發者在程式碼中手動的管理事務的開啟、提交、回滾等操作。

public void test() {

      TransactionDefinition def = new DefaultTransactionDefinition();

      TransactionStatus status = transactionManager.getTransaction(def);

      try {

         // 事務操作

         // 事務提交

         transactionManager.commit(status);

      } catch (DataAccessException e) {

         // 事務提交

         transactionManager.rollback(status);

         throw e;

      }

}
  • 宣告式事務

宣告式事務是基於AOP面向切面,它將具體業務和事務處理部分解耦,程式碼侵入性很低,實際開發中比較常用。我們常用TX和AOP的xml組態檔方式和@Transactional註解方式。

▪宣告式事務的優點:

對程式碼無侵入性,方法內只需要寫業務邏輯,節省很多程式碼量。

▪宣告式事務的缺點:

1、宣告式事務粒度問題:宣告式事務的侷限就是最小粒度要作用在方法上,且不適合耗時長高並行場景。

2、宣告式事務容易被開發者忽略,當事務巢狀的方法中存在RPC遠端呼叫、MQ傳送、Redis更行、檔案寫入等操作可能存在以下場景:

▪ 事務巢狀的方法中RPC呼叫成功了,但是本地事務回滾導致RPC呼叫無法回滾(暫不討論分散式事務)。

▪事務巢狀的方法中遠端呼叫會拉長整個事務週期,導致事務的資料庫連線一致被佔用,類似操作過多會導致資料庫連線池耗盡。

3、宣告式事務使用錯誤會導致在某些場景下失效

四、總結

作者:京東科技 宋慧超

來源:京東雲開發者社群