一篇文章徹底搞懂 Spring 的事務
引言
作爲技術人,很忌諱對某個問題淺嘗輒止,一知半解,特別是經常使用的技術。Spring 作爲經久不衰的 Java 框架,自然少不了對
事務
的支持。那麼,關於Spring 的事務
,你瞭解多少呢?如果只是知道在方法上加一個@Transactional
註解就可以支持事務,或者說只是簡單地知道Spring 聲明式事務
的背後原理是AOP
,恐怕還不夠。今天我們就一起深入瞭解下 Spring 的事務。
一、Srping 中如何開啓事務
Spring 中開啓事務有兩種方式,一種是 聲明式事務,另一種是編程式事務。
- 1. 聲明式事務
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void test() {
// 業務邏輯
}
- 2. 編程式事務
使用 TransactionTemplate
等類和 API 手動管理事務,控制事務的新建、提交、回滾等過程。
- TransactionTemplate
@Resource
private TransactionTemplate transactionTemplate;
@Transactional
public void test() {
transactionTemplate.executeWithoutResult(status -> {
// 業務邏輯
if (something not right) {
// 回滾
status.setRollbackOnly();
}
});
}
- TransactionManager
@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 失效
,從而導致 事務失效
。
-
內部調用
-
類內部方法調用
直接調用原始對象
,根本不涉及代理對象
,所以事務必然失效,但是也可以通過依賴注入自己來規避。 -
非 public 方法、final 方法、靜態方法
-
• 這是一個常被忽略的點,動態代理無法代理
非public方法
、final方法
、靜態方法
,所以這些方法上的事務也會失效。
除了上面講到的那些AOP
的原因,還有一些跟 Spring 事務屬性配置
相關的。
-
rollbackFor 設置錯誤
-
• 這個比較好理解,就是發生異常和設置異常不匹配,導致事務未回滾。
-
propagation 設置錯誤
-
• 這個等下深入探討
其實上面講的都是可能因爲開發人員
人爲疏忽
導致的事務問題,但是正是因爲誰也不能保證開發人員不會犯錯,水平極高,所以只能通過一些開發規範來儘量規避這些問題,使用編程式事務
,就能夠大大減少上述問題的發生。當然,這也不是說聲明式事務完全不能用,只是說不能濫用
。
三、深入探究聲明式事務的原理
其實剛纔還漏講了一個可能導致事務失效
的原因:多線程
,跨線程的事務管理
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
的需求:
-
- 每個線程都有自己獨立的
事務上下文
,事務跟當前調用線程息息相關。
- 每個線程都有自己獨立的
-
- 事務可以設置各種
傳播屬性
(REQUIRED、REQUIRES_NEW 等),也就是同一線程內的方法調用應該可以很方便地訪問當前事務信息,從而決定是否新建事務,換句話說,也就是事務可以嵌套傳播
。
- 事務可以設置各種
-
- 另外,我們也不希望
在方法調用鏈中顯式傳遞事務信息
。
- 另外,我們也不希望
從上面的需求,我們自然而然地想到了 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 的作用
-
事務現場保護和支持事務嵌套
-
• 在
嵌套事務
場景中,保存外層事務的信息。這樣確保內層事務執行完成後,可以恢復到外層事務的上下文。 -
事務上下文的完整性
-
• 維護
ThreadLocal
中事務信息TransactionInfo
的完整性 -
• 形成事務信息的
鏈式結構
,支持多層事務嵌套
。
接着我們再回到入口處,看下 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. PROPAGATION_REQUIRED
-
不掛起
當前事務 -
不創建
新事務 -
• 返回的
TransactionStatus
中transaction
爲現有事務
- 2. PROPAGATION_REQUIRES_NEW
-
掛起
當前事務 -
創建
新事務 -
• 返回的
TransactionStatus
中transaction
爲新事務
這裏的返回值 TransactionStatus
其實就是 事務狀態
,會作爲參數傳給上面的 prepareTransactionInfo()
方法。
public classDefaultTransactionStatusextendsAbstractTransactionStatus {
// 事務名稱
privatefinal String transactionName;
// 事務
privatefinal Object transaction;
// 是否是新事務
privatefinalboolean newTransaction;
// 是否是新的事務同步器
privatefinalboolean newSynchronization;
// 是否嵌套
privatefinalboolean nested;
// 是否只讀
privatefinalboolean readOnly;
// debug標記
privatefinalboolean debug;
// 事務掛起暫存的資源
privatefinal Object suspendedResources;
}
總結:聲明式事務實現原理和過程
一、核心組件
-
TransactionAspectSupport
:事務切面支持類 -
PlatformTransactionManager
:事務管理器 -
TransactionInfo
:事務信息載體 -
ThreadLocal
:事務上下文存儲器
二、實現過程
整體流程圖:
流程說明:
- 1. 事務攔截
- • 通過
AOP
攔截帶有@Transactional
註解的方法
,調用TransactionAspectSupport.invokeWithinTransaction()
。
- 2. 事務準備
-
• 獲取事務屬性 (
TransactionAttribute
) -
• 確定事務管理器 (
PlatformTransactionManager
) -
• 事務上下文管理
-
• 創建
TransactionInfo
並保存事務狀態 -
• 根據
傳播行爲
決定是否創建新事務或者掛起當前事務 -
• 通過
ThreadLocal
存儲事務信息 -
• 支持
事務嵌套
(通過oldTransactionInfo
)
- 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