- 事務的傳播機制
- 多數據源的切換問題
更深入理解 Spring 事務。
用戶註冊完成後,需要給該用戶登記一門PUA必修課,並更新該門課的登記用戶數。
爲此,我添加了兩個表。
課程表 course,記錄課程名稱和註冊的用戶數。
用戶選課表 user_course,記錄用戶表 user 和課程表 course 之間的多對多關聯。
同時爲課程表初始化了一條課程信息
接下來我們完成用戶的相關操作,主要包括兩部分:
-
新增用戶選課記錄
-
課程登記學生數 + 1
新增業務類 CourseService實現相關業務邏輯,分別調用了上述方法保存用戶與課程的關聯關係,並給課程註冊人數+1
爲避免註冊課程的業務異常導致用戶信息無法保存,這裏 catch 註冊課程方法中拋出的異常。希望當註冊課程發生錯誤時,只回滾註冊課程部分,保證用戶信息依然正常。
爲驗證異常是否符合預期,在 regCourse() 裏拋一個註冊失敗異常:
執行代碼:
註冊失敗部分的異常符合預期,但是後面又多了一個這樣的錯誤提示:Transaction rolled back because it has been marked as rollback-only
最後用戶和選課的信息都被回滾了,顯然這不符預期。
期待結果是即便內部事務regCourse()發生異常,外部事務saveStudent()俘獲該異常後,內部事務應自行回滾,不影響外部事務。
這是什麼原因造成的呢?
源碼解析
僞代碼梳理整個事務的結構:
整個業務包含2層事務:
- 外層 saveUser() 的事務
- 內層 regCourse() 事務
Spring聲明式事務中的propagation屬性,表示對這些方法使用怎樣的事務,即:
一個帶事務的方法調用了另一個帶事務的方法,被調用的方法它怎麼處理自己事務和調用方法事務之間的關係。
propagation 有7種配置:
- REQUIRED
默認值,如果本來有事務,則加入該事務,如果沒有事務,則創建新的事務。 - SUPPORTS
- MANDATORY
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- NESTED
因爲:
- 在 saveUser() 上聲明瞭一個外部的事務,就已經存在一個事務了
- 在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)) {
// 是否需要創建一個事務
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 調用具體的業務方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 當發生異常時進行處理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
// 正常返回時提交事務
commitTransactionAfterReturning(txInfo);
return retVal;
}
//......省略非關鍵代碼.....
}
整個方法完成了事務的一整套處理邏輯,如下:
- 檢查是否需要創建事務
- 調用具體的業務方法進行處理
- 提交事務
- 處理異常
當前案例是兩個事務嵌套,外層事務 saveUser()和內層事務 regCourse(),每個事務都會調用到這個方法。所以,該方法會被調兩次。
內層事務
當捕獲了異常,會調用
TransactionAspectSupport.completeTransactionAfterThrowing()
進行異常處理:
對異常類型做了一些檢查,當符合聲明中的定義後,執行具體的 rollback 操作,這個操作是通過如下方法完成:
AbstractPlatformTransactionManager
rollback()
該回滾實現負責處理正參與到已有事務集的事務。委託執行Rollback和doSetRollbackOnly。
繼續調用
processRollback()
該方法裏區分了三種場景:
- 是否有保存點
- 是否爲一個新的事務
- 是否處於一個更大的事務中
因爲默認傳播類型REQUIRED,嵌套的事務並未開啓一個新事務,所以屬於當前事務處於一個更大事務中,所以會走到分支1。
如下的判斷條件確定是否設置爲僅回滾:
if (status.isLocalRollbackOnly() ||
isGlobalRollbackOnParticipationFailure())
滿足任一,都會執行 doSetRollbackOnly():
-
isLocalRollbackOnly
默認 false,當前場景爲 false
-
isGlobalRollbackOnParticipationFailure()
所以,就只由該方法來確定了,默認值爲 true, 即是否回滾交由外層事務統一決定
條件得到滿足,執行
DataSourceTransactionManager#doSetRollbackOnly
最終調用
DataSourceTransactionObject#setRollbackOnly()
內層事務操作執行完畢。
外層事務
外層事務中,業務代碼就捕獲了內層所拋異常,所以該異常不會繼續往上拋,最後的事務會在 TransactionAspectSupport.invokeWithinTransaction()
中的
TransactionAspectSupport#commitTransactionAfterReturning()
該方法裏執行了commit 操作:
AbstractPlatformTransactionManager#commit
當滿足 !shouldCommitOnGlobalRollbackOnly() &&defStatus.isGlobalRollbackOnly()
,就會回滾,否則繼續提交事務:
- shouldCommitOnGlobalRollbackOnly()
若發現事務被標記了全局回滾,且在發生全局回滾時,判斷是否應該提交事務,這個方法的默認返回 false,這裏無需關注 -
isGlobalRollbackOnly()
該方法最終進入
DataSourceTransactionObject#isRollbackOnly()
之前內部事務處理最終調用到DataSourceTransactionObject#setRollbackOnly()
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
- isRollbackOnly()
- setRollbackOnly()
兩個方法本質都是對ConnectionHolder.rollbackOnly
屬性標誌位的存取
但ConnectionHolder則存在於DefaultTransactionStatus#transaction屬性。
綜上:外層事務是否回滾的關鍵,最終取決於DataSourceTransactionObject#isRollbackOnly(),該方法返回值正是在內層異常時設置的。
所以最終外層事務也被回滾,從而在控制檯中打印上述日誌。
這就明白了,Spring默認事務傳播屬性爲REQUIRED:若已有事務,則加入該事務,若無事務,則創建新事務,因而內外兩層事務都處於同一事務。
在 regCourse()中拋異常,並觸發回滾操作時,這個回滾會繼續傳播,從而把 saveUser() 也回滾,最終整個事務都被回滾!
修正
Spring事務默認傳播屬性 REQUIRED,在整個事務的調用鏈上,任一環節拋異常都會導致全局回滾。
所以只需將傳播屬性改成 REQUIRES_NEW :
運行:
異常正常拋出,註冊課程部分的數據沒有保存,但用戶還是正常註冊成功。這意味着此時Spring 只對註冊課程這部分的數據進行了回滾,並沒有傳播到外層:
- 當子事務聲明爲 Propagation.REQUIRES_NEW 時,在
TransactionAspectSupport.invokeWithinTransaction()
中調用createTransactionIfNecessary()
就會創建一個新的事務,獨立於外層事務 - 而在 AbstractPlatformTransactionManager.processRollback() 進行 rollback 處理時,因爲 status.isNewTransaction() 會因爲它處於一個新的事務中而返回 true,所以它走入到了另一個分支,執行了 doRollback() 操作,讓這個子事務單獨回滾,不會影響到主事務。