Spring嵌套事務是怎麼回滾的? 源碼解析 內層事務 外層事務 修正

  • 事務的傳播機制
  • 多數據源的切換問題

更深入理解 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() 操作,讓這個子事務單獨回滾,不會影響到主事務。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章