淺談Spring嵌套事務是怎么回滾的
更深入理解 Spring 事務。
用戶注冊完成后,需要給該用戶登記一門PUA必修課,并更新該門課的登記用戶數(shù)。
為此,我添加了兩個表。
課程表 course,記錄課程名稱和注冊的用戶數(shù)。
用戶選課表 user_course,記錄用戶表 user 和課程表 course 之間的多對多關聯(lián)。
同時為課程表初始化了一條課程信息
接下來我們完成用戶的相關操作,主要包括兩部分:
新增用戶選課記錄
課程登記學生數(shù) + 1
新增業(yè)務類 CourseService實現(xiàn)相關業(yè)務邏輯,分別調用了上述方法保存用戶與課程的關聯(lián)關系,并給課程注冊人數(shù)+1
為避免注冊課程的業(yè)務異常導致用戶信息無法保存,這里 catch 注冊課程方法中拋出的異常。希望當注冊課程發(fā)生錯誤時,只回滾注冊課程部分,保證用戶信息依然正常。
為驗證異常是否符合預期,在 regCourse() 里拋一個注冊失敗異常:
執(zhí)行代碼:
注冊失敗部分的異常符合預期,但是后面又多了一個這樣的錯誤提示:Transaction rolled back because it has been marked as rollback-only
最后用戶和選課的信息都被回滾了,顯然這不符預期。
期待結果是即便內部事務regCourse()發(fā)生異常,外部事務saveStudent()俘獲該異常后,內部事務應自行回滾,不影響外部事務。
這是什么原因造成的呢?
源碼解析
偽代碼梳理整個事務的結構:
整個業(yè)務包含2層事務:
- 外層 saveUser() 的事務
- 內層 regCourse() 事務
Spring聲明式事務中的propagation屬性,表示對這些方法使用怎樣的事務,即:
一個帶事務的方法調用了另一個帶事務的方法,被調用的方法它怎么處理自己事務和調用方法事務之間的關系。
propagation 有7種配置:
- REQUIRED:默認值,如果本來有事務,則加入該事務,如果沒有事務,則創(chuàng)建新的事務。
- SUPPORTS
- MANDATORY
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- NESTED
因為:
- 在 saveUser() 上聲明了一個外部的事務,就已經(jīng)存在一個事務了
- 在propagation值為默認REQUIRED時
regCourse() 就會加入到已有的事務中,兩個方法共用一個事務。
Spring 事務處理的核心:
TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // 是否需要創(chuàng)建一個事務 TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // 調用具體的業(yè)務方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // 當發(fā)生異常時進行處理 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 正常返回時提交事務 commitTransactionAfterReturning(txInfo); return retVal; } //......省略非關鍵代碼..... }
整個方法完成了事務的一整套處理邏輯,如下:
- 檢查是否需要創(chuàng)建事務
- 調用具體的業(yè)務方法進行處理
- 提交事務
- 處理異常
當前案例是兩個事務嵌套,外層事務 saveUser()和內層事務 regCourse(),每個事務都會調用到這個方法。所以,該方法會被調兩次。
內層事務
當捕獲了異常,會調用
TransactionAspectSupport.completeTransactionAfterThrowing()
進行異常處理:
對異常類型做了一些檢查,當符合聲明中的定義后,執(zhí)行具體的 rollback 操作,這個操作是通過如下方法完成:
AbstractPlatformTransactionManager rollback()
該回滾實現(xiàn)負責處理正參與到已有事務集的事務。委托執(zhí)行Rollback和doSetRollbackOnly。
繼續(xù)調用
processRollback()
該方法里區(qū)分了三種場景:
- 是否有保存點
- 是否為一個新的事務
- 是否處于一個更大的事務中
因為默認傳播類型REQUIRED,嵌套的事務并未開啟一個新事務,所以屬于當前事務處于一個更大事務中,所以會走到分支1。
如下的判斷條件確定是否設置為僅回滾:
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure())
滿足任一,都會執(zhí)行 doSetRollbackOnly():
isLocalRollbackOnly
默認 false,當前場景為 falseisGlobalRollbackOnParticipationFailure()
所以,就只由該方法來確定了,默認值為 true, 即是否回滾交由外層事務統(tǒng)一決定
條件得到滿足,執(zhí)行
DataSourceTransactionManager#doSetRollbackOnly
最終調用
DataSourceTransactionObject#setRollbackOnly()
內層事務操作執(zhí)行完畢。
外層事務
外層事務中,業(yè)務代碼就捕獲了內層所拋異常,所以該異常不會繼續(xù)往上拋,最后的事務會在 TransactionAspectSupport.invokeWithinTransaction()
中的
TransactionAspectSupport#commitTransactionAfterReturning()
該方法里執(zhí)行了commit 操作:
AbstractPlatformTransactionManager#commit
當滿足 !shouldCommitOnGlobalRollbackOnly() &&defStatus.isGlobalRollbackOnly()
,就會回滾,否則繼續(xù)提交事務:
shouldCommitOnGlobalRollbackOnly()
若發(fā)現(xiàn)事務被標記了全局回滾,且在發(fā)生全局回滾時,判斷是否應該提交事務,這個方法的默認返回 false,這里無需關注
isGlobalRollbackOnly()
該方法最終進入
DataSourceTransactionObject#isRollbackOnly()
之前內部事務處理最終調用到DataSourceTransactionObject#setRollbackOnly()
public void setRollbackOnly() { getConnectionHolder().setRollbackOnly(); }
- isRollbackOnly()
- setRollbackOnly()
兩個方法本質都是對ConnectionHolder.rollbackOnly
屬性標志位的存取
但ConnectionHolder則存在于DefaultTransactionStatus#transaction屬性。
綜上:外層事務是否回滾的關鍵,最終取決于DataSourceTransactionObject#isRollbackOnly(),該方法返回值正是在內層異常時設置的。
所以最終外層事務也被回滾,從而在控制臺中打印上述日志。
這就明白了,Spring默認事務傳播屬性為REQUIRED:若已有事務,則加入該事務,若無事務,則創(chuàng)建新事務,因而內外兩層事務都處于同一事務。
在 regCourse()中拋異常,并觸發(fā)回滾操作時,這個回滾會繼續(xù)傳播,從而把 saveUser() 也回滾,最終整個事務都被回滾!
修正
Spring事務默認傳播屬性 REQUIRED,在整個事務的調用鏈上,任一環(huán)節(jié)拋異常都會導致全局回滾。
所以只需將傳播屬性改成 REQUIRES_NEW :
運行:
異常正常拋出,注冊課程部分的數(shù)據(jù)沒有保存,但用戶還是正常注冊成功。這意味著此時Spring 只對注冊課程這部分的數(shù)據(jù)進行了回滾,并沒有傳播到外層:
當子事務聲明為 Propagation.REQUIRES_NEW 時,在 TransactionAspectSupport.invokeWithinTransaction()
中調用 createTransactionIfNecessary()
就會創(chuàng)建一個新的事務,獨立于外層事務而在 AbstractPlatformTransactionManager.processRollback() 進行 rollback 處理時,因為 status.isNewTransaction() 會因為它處于一個新的事務中而返回 true,所以它走入到了另一個分支,執(zhí)行了 doRollback() 操作,讓這個子事務單獨回滾,不會影響到主事務。
到此這篇關于淺談Spring嵌套事務是怎么回滾的的文章就介紹到這了,更多相關Spring嵌套事務回滾內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SSH框架網(wǎng)上商城項目第11戰(zhàn)之查詢和刪除商品功能實現(xiàn)
這篇文章主要為大家詳細介紹了SSH框架網(wǎng)上商城項目第11戰(zhàn)之查詢和刪除商品功能實現(xiàn)的相關資料,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06idea中打開項目時import project和open區(qū)別詳解
本文主要介紹了idea中打開項目時import project和open區(qū)別詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-06-06Java實現(xiàn)創(chuàng)建運行時類的對象操作示例
這篇文章主要介紹了Java實現(xiàn)創(chuàng)建運行時類的對象操作,結合實例形式分析了Java動態(tài)創(chuàng)建對象的原理與相關實現(xiàn)技巧,需要的朋友可以參考下2018-08-08go語言題解LeetCode88合并兩個有序數(shù)組示例
這篇文章主要為大家介紹了go語言題解LeetCode88合并兩個有序數(shù)組示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12Java爬蟲實戰(zhàn)抓取一個網(wǎng)站上的全部鏈接
這篇文章主要介紹了JAVA使用爬蟲抓取網(wǎng)站網(wǎng)頁內容的方法,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧。2016-10-10