## **一、前言** emm,又又又踩坑啦。這次的需求主要是對逾期計算的需求任務進行優化,現有的計算任務運行時間太長了。簡單描述下此次的問題:**在項目中進行多個資料庫執行操作時,我們期望的是將其整個封裝成一個事務,要麼全部成功,或者全部失敗,然而在自測異常場景時發現,裡面涉及的第一個數據狀態更新 ...
一、前言
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方法或JDKDynamicAopProxy的invoke方法會間接調用AbstractFallbackTransationAttributeSource的computeTransactionAttribute方法,獲取@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、聲明式事務使用錯誤會導致在某些場景下失效。
四、總結
作者:京東科技 宋慧超
來源:京東雲開發者社區