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

2023-08-23 12:01:58

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

本文會從設計角度,一步步的剖析 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);
}





每個事務/連線使用不同設定

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

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

// 傳播行為設定為 - 方式2,不使用當前事務,獨立一個新事務
// 這個事務/連線中使用 RC 隔離級別,而不是預設的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
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 幾種方式都是基於這個事務管理器的,三中方式的核心實現區別並不大,只是入口不同而已。



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

作者:京東保險 蔣信

來源:京東雲開發者社群 轉載請註明來源