一篇文章徹底搞懂 Spring 的事務

引言

作爲技術人,很忌諱對某個問題淺嘗輒止,一知半解,特別是經常使用的技術。Spring 作爲經久不衰的 Java 框架,自然少不了對事務的支持。那麼,關於 Spring 的事務,你瞭解多少呢?如果只是知道在方法上加一個 @Transactional 註解就可以支持事務,或者說只是簡單地知道 Spring 聲明式事務的背後原理是 AOP,恐怕還不夠。今天我們就一起深入瞭解下 Spring 的事務。

一、Srping 中如何開啓事務

Spring 中開啓事務有兩種方式,一種是 聲明式事務,另一種是編程式事務

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void test() {
    // 業務邏輯
}

使用 TransactionTemplate等類和 API 手動管理事務,控制事務的新建、提交、回滾等過程。

@Resource
private TransactionTemplate transactionTemplate;

@Transactional
public void test() {
    transactionTemplate.executeWithoutResult(status -> {
        // 業務邏輯
        if (something not right) {
            // 回滾
            status.setRollbackOnly();
        }
    });
}
@Resource
private PlatformTransactionManager transactionManager;

@Transactional
publicvoidtest() {
    // 定義事務
    TransactionDefinitiontransactionDefinition=newDefaultTransactionDefinition();
    // 獲取事務狀態
    TransactionStatustransactionStatus= transactionManager.getTransaction(transactionDefinition);

    try {
        // 業務操作
        // 提交事務
        transactionManager.commit(transactionStatus);
    } catch (Exception e) {
        // 異常回滾事務
        transactionManager.rollback(transactionStatus);
        throw e;
    }
}

二、爲什麼不建議用聲明式事務?

上面的兩種事務實現方式,明顯聲明式事務 更簡單更簡潔,對業務也沒有侵入性。但爲什麼說不建議用聲明式事務呢?

當然這個也不是筆者建議的,而是阿里巴巴《Java 開發手冊 v1.5.0 華山版》 建議的。

爲什麼它要這麼建議呢? 答案肯定是聲明式事務 存在着讓人難以忽略的缺點。

事務粒度

首先最容易想到的應該是事務的粒度問題。

因爲聲明式事務能控制的最小粒度方法,整個方法都包含在事務內,但有時這並不是我們想要的,我們希望事務粒度能更小一點,比如說只有某幾行數據庫操作才需要事務。

比如,如果我們將 RPC調用 放入事務方法中,如果事務提交失敗,數據庫最後回滾,但是RPC調用 無法回滾,這就導致了嚴重的數據不一致 問題。

又比如,我們在事務方法中加入太多耗時操作,例如文件操作,更新緩存,發送消息等,就會讓事務變成一個長事務數據庫連接會長時間地被佔用,就可能導致數據庫連接池耗盡,也更容易產生死鎖問題。

更爲關鍵的是,由於這種事務註解很容易被人忽略,並且還存在方法嵌套,所以上面的問題很容易“防不慎防”。相反,使用編程式事務,能讓開發者很清楚地明確事務的邊界

事務失效

另外一個比較重要的問題就是,由於開發者的疏忽或者技術水平的原因,可能會導致聲明式事務失效

在講 事務失效 之前,我們需要簡單瞭解一下 Spring 聲明式事務的原理是什麼,這裏後面會深入探究,這裏先簡單概述一下:

Spring 聲明式事務通過AOP實現,基於@Transactional註解和事務管理器。Spring 使用代理模式(JDK動態代理CGLIB)攔截帶有@Transactional 的方法調用,在方法執行前獲取事務配置,啓動事務;若方法成功執行,提交事務;若發生異常,根據配置回滾事務。這些流程由 TransactionInterceptor 調用PlatformTransactionManager 完成,從而實現自動管理事務邊界

既然 Spring聲明式事務 的實現依賴於 AOP,那麼按照道理說,所有能到導致 AOP 失效 的情況也都會導致聲明式事務事務實效

那麼又是哪些情況會導致 AOP 失效呢?

這就要繼續追問,AOP 的原理是什麼?

Spring 中AOP 的原理就是 JDK動態代理和 CGLIB 代理,有接口的使用 JDK動態代理,沒接口的使用 CGLIB 代理

所以,以下情況不走代理或者無法代理的情況會導致 AOP 失效,從而導致 事務失效

除了上面講到的那些AOP的原因,還有一些跟 Spring 事務屬性配置相關的。

其實上面講的都是可能因爲開發人員人爲疏忽導致的事務問題,但是正是因爲誰也不能保證開發人員不會犯錯,水平極高,所以只能通過一些開發規範來儘量規避這些問題,使用編程式事務,就能夠大大減少上述問題的發生。當然,這也不是說聲明式事務完全不能用,只是說不能濫用

三、深入探究聲明式事務的原理

其實剛纔還漏講了一個可能導致事務失效的原因:多線程跨線程的事務管理 Spring 的聲明式事務也是不支持的。

這裏,我們就要開始從聲明式事務的原理,也就是它是怎麼實現的開始講起了。

我們在上文中簡單提到了 AOP 和 事務管理器 - TransactionManager

我們可以從源碼獲取更多細節。

1、入口類:TransactionAspectSupport

public abstractclassTransactionAspectSupport {
    
    // 核心方法: 在事務環境中執行目標方法
    protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation)throws Throwable {
        // 1. 獲取事務屬性
        TransactionAttributeSourcetas= getTransactionAttributeSource();
        TransactionAttributetxAttr= (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        
        // 2. 確定事務管理器
        TransactionManagertm= determineTransactionManager(txAttr, targetClass);
        
        // 3. 處理響應式事務
        if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
            // 處理響應式事務邏輯...
            return handleReactiveTransaction(/*...*/);
        }
        
        // 4. 處理普通事務 使用 PlatformTransactionManager
        PlatformTransactionManagerptm= asPlatformTransactionManager(tm);
        StringjoinpointIdentification= methodIdentification(method, targetClass, txAttr);
        
        // 5. 執行事務處理
        if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
            // 標準事務處理流程:
            TransactionInfotxInfo= createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
            ObjectretVal=null;
            try {
                // 執行目標方法
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // 異常回滾
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                if (txInfo != null) {
                    txInfo.restoreThreadLocalStatus();
                }
            }
            // 提交事務
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }
        else {
            // 回調式事務處理...
        }
    }
}

從上面我們可以很清晰地看到整體事務處理流程。

我們先從 "5. 執行事務處理" 中的很重要的一個類 TransactionInfo 說起,看下它裏面存的是什麼?

protected staticfinalclassTransactionInfo {
    // 事務管理器
    privatefinal PlatformTransactionManager transactionManager;
    // 事務屬性(傳播行爲、隔離級別等配置)
    privatefinal TransactionAttribute transactionAttribute;
    // 方法標識(用於日誌)
    privatefinal String joinpointIdentification;
    // 當前事務狀態
    private TransactionStatus transactionStatus;
    // 父事務信息
    private TransactionInfo oldTransactionInfo;
}

TransactionInfo 存儲的就是事務信息,它的主要作用是:

不過,這個事務信息 TransactionInfo 保存在哪裏呢?這是一個很重要的問題。

我們來梳理下 TransactionInfo 的需求:

    1. 每個線程都有自己獨立的事務上下文,事務跟當前調用線程息息相關。
    1. 事務可以設置各種傳播屬性(REQUIRED、REQUIRES_NEW 等),也就是同一線程內的方法調用應該可以很方便地訪問當前事務信息,從而決定是否新建事務,換句話說,也就是 事務可以嵌套傳播
    1. 另外,我們也不希望在方法調用鏈中顯式傳遞事務信息

從上面的需求,我們自然而然地想到了 Java 中的 ThreadLocal,它用來保存事務信息再合適不過,事實也的確如此。

我們看下 prepareTransactionInfo() -> bindToThread() ,其實就是將當前事務信息 TransactionInfo 保存到 ThreadLocal 中。

// 父事務信息
private TransactionInfo oldTransactionInfo;

// 保存當前調用線程的事務信息
privatestaticfinal ThreadLocal<TransactionInfo> transactionInfoHolder = 
      newNamedThreadLocal<>("Current aspect-driven transaction");

// 將當前事務,放入事務上下文 ThreadLocal
privatevoidbindToThread() {
    // 暫存父事務到 oldTransactionInfo
    this.oldTransactionInfo = transactionInfoHolder.get();
    // 保存當前事務到 ThreadLocal
    transactionInfoHolder.set(this);
}

這裏我們也就明白了爲什麼一開始說,多線程也會導致聲明式事務失效,因爲 ThreadLocal 保存的內容不能跨線程

接着我們繼續回到 TransactionInfo,它裏面其它幾個屬性非常好理解,但是其中 oldTransactionInfo 讓人覺得有點奇怪,它到底有什麼用?其實它非常重要,具有特殊作用。

我們先看下面這個場景:

@Transactional
publicvoidouter() {
    // 創建 TransactionInfo1
    inner();  // 調用內層事務方法
    // 恢復到 TransactionInfo1
}

@Transactional
publicvoidinner() {
    // 創建 TransactionInfo2
    // oldTransactionInfo 指向 TransactionInfo1
    // 方法結束時恢復到 TransactionInfo1
}

看到這裏是不是恍然大悟,在這種方法嵌套中,要怎麼處理父事務子事務 ?答案就在 oldTransactionInfo 屬性。

private void bindToThread() {
    // 取出 transactionInfoHolder(ThreadLocal) 中的父事務 TransactionInfo1 到 oldTransactionInfo
    this.oldTransactionInfo = transactionInfoHolder.get();
    // 將新事務 TransactionInfo2 保存到 transactionInfoHolder
    transactionInfoHolder.set(this);
}

private void restoreThreadLocalStatus() {
    // 處理完了 TransactionInfo2,恢復之前保存的父事務 TransactionInfo1
    transactionInfoHolder.set(this.oldTransactionInfo);
}

小結: oldTransactionInfo 的作用

接着我們再回到入口處,看下 createTransactionIfNecessary 方法,這個方法也比較關鍵,從中可以看出我們的事務是如何創建的,以及在註解中設置的 propagation 屬性在這裏會起什麼作用。

protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm,
        TransactionAttribute txAttr, final String joinpointIdentification) {

    // 如果事務沒有指定名稱,使用方法名稱作爲事務名稱
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = newDelegatingTransactionAttribute(txAttr)....
    }

    // 從事務管理器獲取事務狀態
    TransactionStatusstatus=null;
    // 事務屬性不爲空
    if (txAttr != null) {
        //存在事務管理器
        if (tm != null) {
            // 根據指定的傳播行爲,返回當前活躍的事務或者新建一個事務
            status = tm.getTransaction(txAttr);
        }
    }
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

我們先跳過

PlatformTransactionManager#getTransaction

先看下

TransactionAspectSupport#prepareTransactionInfo

protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
        @Nullable TransactionAttribute txAttr, String joinpointIdentification,
        @Nullable TransactionStatus status) {

    // 1. 創建事務信息對象
    TransactionInfotxInfo=newTransactionInfo(tm, txAttr, joinpointIdentification);
    
    if (txAttr != null) {
        // 2. 如果有事務屬性配置,說明需要事務
        // 3. 設置事務狀態
        txInfo.newTransactionStatus(status);
    }
    else {
        // 4. 沒有事務屬性,說明不需要事務
    }

    // 5. 重要:總是綁定到ThreadLocal
    txInfo.bindToThread();
    return txInfo;
}

prepareTransactionInfo 主要就是創建 TransactionInfo 對象(包含:事務管理器事務屬性方法標識符等),維護事務狀態,並且綁定到 ThreadLocal 。

事務管理器:PlatformTransactionManager

現在繼續深入到

PlatformTransactionManager#getTransaction(TransactionDefinition)

PlatformTransactionManager 其實是個接口,所以要看繼承它的抽象類:

AbstractPlatformTransactionManager#getTransaction

public final TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException {

    // 如果沒有事務定義使用默認值
    TransactionDefinitiondef= (definition != null ? definition : TransactionDefinition.withDefaults());

    // 獲取事務,不同的ORM框架(JDBC、JPA、Hibernate等)獲取事務的方式可能不同,交由子類去實現
    Objecttransaction= doGetTransaction();

    // 如果當前已經存在事務,並且該事務處於活躍狀態
    if (isExistingTransaction(transaction)) {
        // 根據設置的傳播行爲決定如何處理
        return handleExistingTransaction(def, transaction, debugEnabled);
    }

    // 爲新事務檢查超時時間設置
    if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        thrownewInvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
    }

    // 當前不存在事務,根據設置的傳播行爲決定如何處理,如果設定爲 PROPAGATION_MANDATORY,拋出異常
    if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        thrownewIllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    // 如果是其它的,開啓一個新事務
    elseif (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        SuspendedResourcesHoldersuspendedResources= suspend(null);
    
        try {
            // 開啓新事務
            return startTransaction(def, transaction, false, debugEnabled, suspendedResources);
        }
        catch (RuntimeException | Error ex) {
            // 恢復掛起資源
            resume(null, suspendedResources);
            throw ex;
        }
    }
    else {
        // 創建“空”事務:沒有實際事務,但可能會有事務同步器。
        booleannewSynchronization= (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
    }
}

接着看看 handleExistingTransaction ,看下如果當前事務上下文存在活躍事務會如何處理。

private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled)
        throws TransactionException {

    // 如果傳播行爲是 PROPAGATION_NEVER,拋出異常
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
        thrownewIllegalTransactionStateException...
    }

    // 如果傳播行爲是 PROPAGATION_NOT_SUPPORTED,直接在非事務中運行
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
        // 掛起當前事務
        ObjectsuspendedResources= suspend(transaction);
        booleannewSynchronization= (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        // 第二個參數傳遞null,表示不需要事務
        return prepareTransactionStatus(
                definition, null, false, newSynchronization, debugEnabled, suspendedResources);
    }

    // 如果是 PROPAGATION_REQUIRES_NEW,
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        // 掛起當前事務
        SuspendedResourcesHoldersuspendedResources= suspend(transaction);
        try {
            // 開啓新事務
            return startTransaction(definition, transaction, false, debugEnabled, suspendedResources);
        }
        catch (RuntimeException | Error beginEx) {
            // 出現異常,恢復掛起事務
            resumeAfterBeginException(transaction, suspendedResources, beginEx);
            throw beginEx;
        }
    }

    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // 省略
    }

    // PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY:
    // 原事務是否有效
    if (isValidateExistingTransaction()) {
        // 新加入的事務必須與原事務隔離級別相同 否則報錯
        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
            IntegercurrentIsolationLevel= TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
            if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
                thrownewIllegalTransactionStateException...
            }
        }
        // 當前事務非只讀事務 但是已經存在的事務是隻讀 報錯
        if (!definition.isReadOnly()) {
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                thrownewIllegalTransactionStateException...
            }
        }
    }

    // 事務同步器:提供了事務生命週期的鉤子方法 用於資源管理、狀態清理、監控等場景
    booleannewSynchronization= (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    // 根據給定參數創建新的事務狀態
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}

可以看出

AbstractPlatformTransactionManager#getTransaction

會根據不同的事務傳播行爲做出不同行爲,創建新事務,或者使用現有事務,或者掛起當前事務,或者拋出異常等。

就比如PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW的在處理上區別如下:

  1. 1. PROPAGATION_REQUIRED
  1. 2. PROPAGATION_REQUIRES_NEW

這裏的返回值 TransactionStatus 其實就是 事務狀態,會作爲參數傳給上面的 prepareTransactionInfo() 方法。

public classDefaultTransactionStatusextendsAbstractTransactionStatus {
    // 事務名稱
    privatefinal String transactionName;
    // 事務
    privatefinal Object transaction;
    // 是否是新事務
    privatefinalboolean newTransaction;
    // 是否是新的事務同步器
    privatefinalboolean newSynchronization;
    // 是否嵌套
    privatefinalboolean nested;
    // 是否只讀
    privatefinalboolean readOnly;
    // debug標記
    privatefinalboolean debug;
    // 事務掛起暫存的資源
    privatefinal Object suspendedResources;
}

總結:聲明式事務實現原理和過程

一、核心組件

二、實現過程

整體流程圖:

流程說明

  1. 1. 事務攔截
  1. 2. 事務準備
  1. 3. 方法執行
try {
    // 執行業務方法
    retVal = invocation.proceedWithInvocation();
    // 提交事務
    commitTransactionAfterReturning();
} catch (Exception ex) {
    // 回滾事務
    completeTransactionAfterThrowing();
    throw ex;
} finally {
    // 清理事務信息
    cleanupTransactionInfo();
}

四、場景題

最後,我們出一個場景題來看下你對剛纔的事務的傳播行爲的理解。

@Service
publicclassDemoServiceA {

    @Resource
    private DemoServiceB demoServiceB;

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    publicvoida() {
        // 步驟A:寫數據庫1
        // 步驟B:寫數據庫2
        // 步驟C:調用 DemoServiceB.b() 方法讀數據
        vardata= demoServiceB.b();
    }

}

@Service
publicclassDemoServiceB {

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.?)
    public Object b() {
        // read data from db
    }

}

在上面的場景中,demoServiceB 的 b() 方法的傳播行爲-propagation 應該設置爲什麼呢?

答案是應該設置爲:

Propagation.NOT_SUPPORTED

因爲這樣的話,如果步驟C 讀取數據失敗,不會導致步驟A 和 步驟B 中數據修改回滾

那如果是下面這樣呢?

調用 demoServiceB.b() 步驟在中間。

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void a() {
      // 步驟A:寫數據庫1
      // 步驟B:調用 DemoServiceB.b() 方法讀數據
      var data = demoServiceB.b();
      // 步驟C:寫數據庫2
}

答案是應該設置爲:

Propagation.REQUIRED

因爲這樣的話,如果步驟B 讀取數據失敗,步驟C 還沒開始,步驟A 修改了的數據應該回滾的。


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