從頭到尾說一次 Spring 事務管理(器)

事務管理,一個被說爛的也被看爛的話題,還是八股文中的基礎股之一。

本文會從設計角度,一步步的剖析 Spring 事務管理的設計思路(都會設計事務管理器了,還能玩不轉?)

一、爲什麼需要事務管理?

先看看如果沒有事務管理器的話,如果想讓多個操作(方法 / 類)處在一個事務裏應該怎麼做:

// MethodA:
public void methodA(){
  Connection connection = acquireConnection();
    try{
        int updated = connection.prepareStatement().executeUpdate();
        methodB(connection);
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        releaseConnection(connection);
    }
}

// MethodB:
public void methodB(Connection connection){
  int updated = connection.prepareStatement().executeUpdate();
}

或者用 ThreadLocal 存儲 Connection?

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

// MethodA:
public void methodA(){
  Connection connection = acquireConnection();
  connHolder.set(connection);
    try{
        int updated = connection.prepareStatement().executeUpdate();
        methodB();
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        releaseConnection(connection);
        connHolder.remove();
    }
}

// MethodB:
public void methodB(){
    Connection connection = connHolder.get();
  int updated = connection.prepareStatement().executeUpdate();
}

還是有點噁心,再抽象一下?將綁定 Connection 的操作提取爲公共方法:

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

private void bindConnection(){
  Connection connection = acquireConnection();
    connHolder.set(connection);
}

private void unbindConnection(){
  releaseConnection(connection);
    connHolder.remove();
}

// MethodA:
public void methodA(){
    try{
        bindConnection();
        int updated = connection.prepareStatement().executeUpdate();
        methoB();
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        unbindConnection();
    }
}

// MethodB:
public void methodB(){
    Connection connection = connHolder.get();
  int updated = connection.prepareStatement().executeUpdate();
}

現在看起來好點了,不過我有一個新的需求:想讓 methodB 獨立一個新事務,單獨提交和回滾,不影響 methodA

這可就有點難搞了,ThreadLocal 中已經綁定了一個 Connection,再新事務的話就不好辦了

那如果再複雜點呢,methodB 中需要調用 methodC,methodC 也需要一個獨立事務。

而且,每次 bind/unbind 的操作也有點太傻了,萬一哪個方法忘了寫 unbind ,最後來一個連接泄露那不是完蛋了!

好在 Spring 提供了事務管理器,幫我們解決了這一系列痛點。

二、Spring 事務管理解決了什麼問題?

Spring 提供的事務管理可以幫我們管理事務相關的資源,比如 JDBC 的 Connection、Hibernate 的 Session、Mybatis 的 SqlSession。如說上面的 Connection 綁定到 ThreadLocal 來解決共享一個事務的這種方式,Spring 事務管理就已經幫我們做好了。

還可以幫我們處理複雜場景下的嵌套事務,比如前面說到的 methodB/methodC 獨立事務。

什麼是嵌套事務?

還是拿上面的例子來說, methodA 中調用了 methodB,兩個方法都有對數據庫的操作,而且都需要事務:


// MethodA:
public void methodA(){
    int updated = connection.prepareStatement().executeUpdate();
    methodB();
    // ...
}

// MethodB:
public void methodB(){
    // ...
}

這種多個方法調用鏈中都有事務的場景,就是嵌套事務。不過要注意的是,並不是說多個方法使用一個事務才叫嵌套,哪怕是不同的事務,只要在這個方法的調用鏈中,都是嵌套事務。

什麼是事務傳播行爲?

那調用鏈中的子方法,是用一個新事務,還是使用當前事務呢?這個子方法決定使用新事務還是當前事務(或不使用事務)的策略,就叫事務傳播。

在 Spring 的事務管理中,這個子方法的事務處理策略叫做事務傳播行爲(Propogation Behavior)。

有哪些事務傳播行爲?

Spring 的事務管理支持多種傳播行爲,這裏就不貼了,八股文裏啥都有。

但給這些傳播行爲分類之後,無非是以下三種:

  1. 優先使用當前事務

  2. 不使用當前事務,新建事務

  3. 不使用任何事務

比如上面的例子中,methodB/methodC 獨立事務,就屬於第 2 種傳播行爲 - 不使用當前事務,新建事務

看個栗子

以 Spring JDBC + Spring 註解版的事務舉例。在默認的事務傳播行爲下,methodA 和 methodB 會使用同一個 Connection,在一個事務中


@Transactional
public void methodA(){
    jdbcTemplate.batchUpdate(updateSql, params);
    methodB();
}

@Transactional
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

如果我想讓 methodB 不使用 methodA 的事務,自己新建一個連接 / 事務呢?只需要簡單的配置一下 @Transactional 註解:


@Transactional
public void methodA(){
    jdbcTemplate.batchUpdate(updateSql, params);
    methodB();
}

// 傳播行爲配置爲 - 方式2,不使用當前事務,獨立一個新事務
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

就是這麼簡單,獲取 Connection / 多方法共享 Connection / 多方法共享 + 獨享 Connection / 提交 / 釋放連接之類的操作,完全不需要我們操心,Spring 都替我們做好了。

怎麼回滾?

在註解版的事務管理中,默認的的回滾策略是:拋出異常就回滾。這個默認策略挺好,連回滾都幫我們解決了,再也不用手動回滾。

但是如果在嵌套事務中,子方法獨立新事務呢?這個時候哪怕拋出異常,也只能回滾子事務,不能直接影響前一個事務

可如果這個拋出的異常不是 sql 導致的,比如校驗不通過或者其他的異常,此時應該將當前的事務回滾嗎?

這個還真不一定,誰說拋異常就要回滾,異常也不回滾行不行?

當然可以!拋異常和回滾事務本來就是兩個問題,可以連在一起,也可以分開處理


// 傳播行爲配置爲 - 方式2,不使用當前事務,獨立一個新事務

// 指定 Exception 也不會滾
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

每個事務 / 連接使用不同配置

除了傳播和回滾之外,還可以給每個事務 / 連接使用不同的配置,比如不同的隔離級別:

除了隔離級別之外,其他的 JDBC Connection 配置當然也是支持的,比如 readOnly。這樣一來,雖然我們不用顯示的獲取 connection/session,但還是可以給嵌套中的每一個事務配置不同的參數,非常靈活。

功能總結

好了,現在已經瞭解了 Spring 事務管理的所有核心功能,來總結一下這些核心功能點:

  1. 連接 / 資源管理 - 無需手動獲取資源、共享資源、釋放資源

  2. 嵌套事務的支持 - 支持嵌套事務中使用不同的資源策略、回滾策略

  3. 每個事務 / 連接使用不同的配置

三、事務管理器(TransactionManager)模型

其實仔細想想,事務管理的核心操作只有兩個:提交和回滾。前面所謂的傳播、嵌套、回滾之類的,都是基於這兩個操作。

所以 Spring 將事務管理的核心功能抽象爲一個事務管理器(Transaction Manager),基於這個事務管理器核心,可以實現多種事務管理的方式。

這個核心的事務管理器只有三個功能接口:

  1. 獲取事務資源,資源可以是任意的,比如 jdbc connection/hibernate mybatis session 之類,然後綁定並存儲

  2. 提交事務 - 提交指定的事務資源

  3. 回滾事務 - 回滾指定的事務資源

interface PlatformTransactionManager{
    // 獲取事務資源,資源可以是任意的,比如jdbc connection/hibernate mybatis session之類
  TransactionStatus getTransaction(TransactionDefinition definition)
      throws TransactionException;
    
    // 提交事務
    void commit(TransactionStatus status) throws TransactionException;
    
    // 回滾事務
    void rollback(TransactionStatus status) throws TransactionException;
}

事務定義 - TransactionDefinition

還記得上面的 @Transactional 註解嗎,裏面定義了傳播行爲、隔離級別、回滾策略、只讀之類的屬性,這個就是一次事務操作的定義。

在獲取事務資源時,需要根據這個事務的定義來進行不同的配置:

  1. 比如配置了使用新事務,那麼在獲取事務資源時就需要創建一個新的,而不是已有的

  2. 比如配置了隔離級別,那麼在首次創建資源(Connection)時,就需要給 Connection 設置 propagation

  3. 比如配置了只讀屬性,那麼在首次創建資源(Connection)時,就需要給 Connection 設置 readOnly

爲什麼要單獨用一個 TransactionDefinition 來存儲事務定義,直接用註解的屬性不行嗎?

當然可以,但註解的事務管理只是 Spring 提供的自動擋,還有適合老司機的手動擋事務管理(後面會介紹);手動擋可用不了註解,所以單獨建一個事務定義的模型,這樣就可以實現通用。

事務狀態 - TransactionStatus

那既然嵌套事務下,每個子方法的事務可能不同,所以還得有一個子方法事務的狀態 - TransactionStatus,用來存儲當前事務的一些數據和狀態,比如事務資源(Connection)、回滾狀態等。

獲取事務資源

事務管理器的第一步,就是根據事務定義來獲取 / 創建資源了,這一步最麻煩的是要區分傳播行爲,不同傳播行爲下的邏輯不太一樣。

“默認的傳播行爲下,使用當前事務”,怎麼算有當前事務呢?

把事務資源存起來嘛,只要已經存在那就是有當前事務,直接獲取已存儲的事務資源就行。文中開頭的例子也演示了,如果想讓多個方法無感的使用同一個事務,可以用 ThreadLocal 存儲起來,簡單粗暴。

Spring 也是這麼做的,不過它實現的更復雜一些,抽象了一層事務資源同步管理器 - TransactionSynchronizationManager(本文後面會簡稱 TxSyncMgr),在這個同步管理器裏使用 ThreadLocal 存儲了事務資源(本文爲了方便理解,儘可能的不貼非關鍵源碼)。

剩下的就是根據不同傳播行爲,執行不同的策略了,分類之後只有 3 個條件分支:

  1. 當前有事務 - 根據不同傳播行爲處理不同

  2. 當前沒事務,但需要開啓新事務

  3. 徹底不用事務 - 這個很少用


public final TransactionStatus getTransaction(TransactionDefinition definition) {
    //創建事務資源 - 比如 Connection
    Object transaction = doGetTransaction();
    
    if (isExistingTransaction(transaction)) {
        // 處理當前已有事務的場景
        return handleExistingTransaction(def, transaction, debugEnabled);
    }else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
        def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
        def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){
        
        // 開啓新事務
      return startTransaction(def, transaction, debugEnabled, suspendedResources);
    }else {
      // 徹底不用事務
    }
    
    // ...
}

先介紹一下分支 2 - 當前沒事務,但需要開啓新事務,這個邏輯相對簡單一些。只需要新建事務資源,然後綁定到 ThreadLocal 即可:


private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
      boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {
    
      // 創建事務
    DefaultTransactionStatus status = newTransactionStatus(
        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
      
      // 開啓事務(beginTx或者setAutoCommit之類的操作)
      // 然後將事務資源綁定到事務資源管理器 TransactionSynchronizationManager
    doBegin(transaction, definition);

現在回到分支 1 - 當前有事務 - 根據不同傳播行爲處理不同,這個就稍微有點麻煩了。因爲有子方法獨立事務的需求,可是 TransactionSynchronizationManager 卻只能存一個事務資源。

掛起(Suspend)和恢復(Resume)

Spring 採用了一種掛起 (Suspend) - 恢復(Resume) 的設計來解決這個嵌套資源處理的問題。當子方法需要獨立事務時,就將當前事務掛起,從 TxSyncMgr 中移除當前事務資源,創建新事務的狀態時,將掛起的事務資源保存至新的事務狀態 TransactionStatus 中;在子方法結束時,只需要再從子方法的事務狀態中,再次拿出掛起的事務資源,重新綁定至 TxSyncMgr 即可完成恢復的操作。

整個掛起 - 恢復的流程,如下圖所示:

注意:掛起操作是在獲取事務資源這一步做的,而恢復的操作是在子方法結束時(提交或者回滾)中進行的。

這樣一來,每個 TransactionStatus 都會保存掛起的前置事務資源,如果方法調用鏈很長,每次都是新事務的話,那這個 TransactionStatus 看起來就會像一個鏈表:

提交事務

獲取資源、操作完畢後來到了提交事務這一步,這個提交操作比較簡單,只有兩步:

  1. 當前是新事務才提交

  2. 處理掛起資源

怎麼知道是新事務?

每經過一次事務嵌套,都會創建一個新的 TransactionStatus,這個事務狀態裏會記錄當前是否是新事務。如果多個子方法都使用一個事務資源,那麼除了第一個創建事務資源的 TransactionStatus 之外,其他都不是新事務。

如下圖所示,A -> B -> C 時,由於 BC 都使用當前事務,那麼雖然 ABC 所使用的事務資源是一樣的,但是隻有 A 的 TransactionStatus 是新事務,BC 並不是;那麼在 BC 提交事務時,就不會真正的調用提交,只有回到 A 執行 commit 操作時,纔會真正地調用提交操作。

這裏再解釋下,爲什麼新事務才需要提交,而已經有事務卻什麼都不用做:

因爲對於新事務來說,這裏的提交操作已經是事務完成了;而對於非新事務的場景,前置事務(即當前事務)還沒有執行完,可能後面還有其他數據庫操作,所以這個提交的操作得讓當前事務創建方去做,這裏並不能提交。

回滾事務

除了提交,還有回滾呢,回滾事務的邏輯和提交事務類似:

  1. 如果是新事務纔回滾,原因上面已經介紹過了

  2. 如果不是新事務則只設置回滾標記

  3. 處理掛起資源

注意: 事務管理器是不包含回滾策略這個東西的,回滾策略是 AOP 版的事務管理增強的功能,但這個功能並不屬於核心的事務管理器。

四、自動擋與手動擋

Spring 的事務管理功能都是圍繞着上面這個事務管理器運行的,提供了三種管理事務的方式,分別是:

  1. XML AOP 的事務管理 - 比較古老現在用的不多

  2. 註解版本的事務管理 - @Transactional

  3. TransactionTemplate - 手動擋的事務管理,也稱編程式事務管理

自動擋

XML/@Transactional 兩種基於 AOP 的註解管理,其入口類是 TransactionInterceptor,是一個 AOP 的 Interceptor,負責調用事務管理器來實現事務管理。

因爲核心功能都在事務管理器裏實現,所以這個 AOP Interceptor 很簡單,只是調用一下事務管理器,核心(僞)代碼如下:

public Object invoke(MethodInvocation invocation) throws Throwable {
    
    // 獲取事務資源
  Object transaction = transactionManager.getTransaction(txAttr);    
    Object retVal;
    
    try {
        // 執行業務代碼
      retVal = invocation.proceedWithInvocation();
        
        // 提交事務
        transactionManager.commit(txStatus);
    } catch (Throwable ex){
        // 先判斷異常回滾策略,然後調用事務管理器的 rollback
      rollbackOn(ex, txStatus);
    } 
}

並且 AOP 這種自動擋的事務管理還增加了一個回滾策略的玩法,這個是手動擋 TransactionTemplate 所沒有的,但這個功能並不在事務管理器中,只是 AOP 版事務的一個增強。

手動擋

TransactionTemplate 這個是手動擋的事務管理,雖然沒有註解的方便,但是好在靈活,異常 / 回滾啥的都可以自己控制。

所以這個實現更簡單,連異常回滾策略都沒有,特殊的回滾方式還要自己設置(默認是任何異常都會回滾),核心(僞)代碼如下:


public <T> T execute(TransactionCallback<T> action) throws TransactionException {
  
    // 獲取事務資源
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {
        
        // 執行 callback 業務代碼
        result = action.doInTransaction(status);
    }
    catch (Throwable ex) {
        
        // 調用事務管理器的 rollback
        rollbackOnException(status, ex);
    }
    
    提交事務
    this.transactionManager.commit(status);
  }
}

爲什麼有這麼方便的自動擋,還要手動擋?

因爲手動擋更靈活啊,想怎麼玩就怎麼玩,比如我可以在一個方法中,執行多個數據庫操作,但使用不同的事務資源:


Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                       new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
      // update 0
            int rows0 = jdbcTemplate.update(...);
            
            // update 1
            int rows1 = jdbcTemplate.update(...);
            return rows0 + rows1;
        }
    });

Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
            
            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });

在上面這個例子裏,通過 TransactionTemplate 我們可以精確的控制 update0/update1 使用同一個事務資源和隔離級別,而 update2 單獨使用一個事務資源,並且不需要新建類加註解的方式。

手自一體可以嗎?

當然可以,只要我們使用的是同一個事務管理器的實例,因爲綁定資源到同步資源管理器這個操作是在事務管理器中進行的。

AOP 版本的事務管理裏,同樣可以使用手動擋的事務管理繼續操作,而且還可以使用同一個事務資源 。

比如下面這段代碼,update1/update2 仍然在一個事務內,並且 update2 的 callback 結束後並不會提交事務,事務最終會在 methodA 結束時,TransactionInterceptor 中才會提交


@Transactional
public void methodA(){
    
    // update 1
  jdbcTemplate.update(...);
    new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
            
            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });
   
}

五、總結


Spring 的事務管理,其核心是一個抽象的事務管理器,XML/@Transactional/TransactionTemplate 幾種方式都是基於這個事務管理器的,三種方式的核心實現區別並不大,只是入口不同而已。

本文爲了方便理解,省略了大量的非關鍵實現細節,可能會導致有部分描述不嚴謹的地方,如有問題歡迎評論區留言。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0X2BnZXfxauIb2-fVvHdFQ