開發中我們經常會用到 Spring Boot 的事務注解,為含有多種操作的方法添加事務,做到如果某一個環節出錯,全部回滾的效果。但是在開發中可能會因為不了解事務機制,而導致我們的方法使用了 @Transactional
注解但是沒有生效的情況,下面就把這幾種不能生效的情況整理一下。
文章目錄
- 一、非public方法(動態代理限制)
- 二、自調用問題(類內部方法調用,不走代理)
- 三、異常類型不匹配(默認只回滾RuntimeException)
- 四、多線程切換(事務連接綁定ThreadLocal)
- 五、錯誤傳播行為(如:PROPAGATION_NOT_SUPPORTED掛起事務)
- 六、總結
一、非public方法(動態代理限制)
Spring 的事務管理本質上是通過 AOP 動態代理 實現的(JDK 動態代理或 CGLIB 代理)。
代理對象在調用目標方法時,會添加事務管理的邏輯(開啟事務、提交/回滾事務)。
然而,動態代理只能代理 public
方法。
如果你將 @Transactional
注解放在 protected
、private
或默認(包級私有)方法上,Spring 在創建代理時無法為這些方法添加事務增強邏輯。
當你通過代理對象調用這些非 public
方法時,事務相關的代碼(如 beginTransaction()
, commit()
, rollback()
)不會被織入,因此事務管理完全失效。
所以,要確保所有需要事務管理的方法都是 public
的。這是 Spring AOP 代理機制的一個硬性限制。
二、自調用問題(類內部方法調用,不走代理)
這是 AOP 代理機制帶來的另一個典型問題。假設一個 Service
類中有兩個方法:
methodA()
:沒有@Transactional
注解。methodB()
:有@Transactional
注解。
如果你在 methodA()
內部直接調用 this.methodB()
,那么你調用的是 Service
類本身的 methodA()
(this
指向目標對象本身)。methodA()
內部調用 this.methodB()
,是目標對象內部的方法調用。
這個調用完全不經過為該 Service
類生成的代理對象。
因為調用 methodB()
沒有經過代理對象,所以代理對象上附加的事務攔截邏輯根本不會被執行。methodB()
雖然標注了 @Transactional
,但在此次調用中完全失效。
解決方案有以下幾種:推薦重構代碼。
方案一:注入自身代理對象
開啟 exposeProxy
:在配置類(如 @SpringBootApplication
主類)上添加 @EnableAspectJAutoProxy(exposeProxy = true)
。
在需要自調用事務方法的地方獲取代理對象:
((YourServiceClass) AopContext.currentProxy()).methodB();
AopContext.currentProxy()
獲取到當前方法執行上下文中的代理對象(即被 Spring AOP 增強過的對象),通過這個代理對象調用 methodB()
,就會走代理邏輯,事務攔截器生效。
這種方式不常用,會有缺點,引入了 Spring AOP 特定 API (AopContext
),增加了代碼耦合度。
方案二:重構代碼(推薦)
將需要事務管理的業務邏輯 methodB()
抽取到另一個獨立的 Bean(如另一個 Service
)中。然后在原來的 methodA()
中注入并使用這個新的 Bean 來調用 methodB()
。這樣調用自然通過代理對象進行。
這是更符合設計原則(單一職責、依賴注入)的做法,避免了自調用問題,也降低了耦合。
方案三:使用 ApplicationContext
獲取 Bean
在類中注入 ApplicationContext
,然后通過 ctx.getBean(YourServiceClass.class).methodB()
來調用。這樣獲取到的是代理 Bean,調用會走代理。
代碼略顯繁瑣,并且也需要依賴 Spring 容器。
三、異常類型不匹配(默認只回滾RuntimeException)
@Transactional
注解的 rollbackFor
屬性默認值是 RuntimeException
和 Error
。
- 當方法拋出
RuntimeException
或其子類(如NullPointerException
,IllegalArgumentException
)時,Spring 會回滾事務。 - 當方法拋出檢查型異常(如
IOException
,SQLException
)時,Spring 默認會提交事務!
如果你在一個事務方法中拋出了自定義的業務異常(繼承自 Exception
而非 RuntimeException
),或者拋出了其他檢查型異常,并且沒有顯式配置 rollbackFor
,那么即使業務邏輯出錯拋出了異常,Spring 也會正常提交事務,導致數據不一致。
這時,我們要顯式指定 rollbackFor
:在 @Transactional
注解中明確聲明哪些異常需要觸發回滾。
// 回滾所有 Exception 和自定義異常
@Transactional(rollbackFor = {Exception.class, YourCustomBusinessException.class})
public void transactionalMethod() throws Exception { ... }
或者修改默認行為(謹慎):雖然不推薦,但可以通過修改 Spring 的全局事務管理器配置來改變默認的回滾異常類型(例如改為回滾所有 Throwable
)。
但這樣做風險較大,可能回滾不應該回滾的異常(如 OutOfMemoryError
)。
最佳實踐還是根據具體業務在注解上顯式配置 rollbackFor
和 noRollbackFor
。
四、多線程切換(事務連接綁定ThreadLocal)
Spring 的事務管理核心是將數據庫連接(Connection
)綁定到當前執行線程(Thread
)的 ThreadLocal
變量上。
一個事務從開始(beginTransaction
)到提交/回滾(commit
/rollback
)期間,所有數據庫操作都使用這個綁定在當前線程 ThreadLocal
上的同一個 Connection
,以此保證 ACID 特性。
如果你在一個事務方法內部啟動了一個新線程(new Thread()
) 或者使用線程池(如 @Async
)執行數據庫操作,會出現以下情況:
- 新線程擁有自己獨立的
ThreadLocal
存儲。 - 新線程無法訪問到原始事務線程綁定的
Connection
對象。 - 新線程中的數據庫操作會從連接池獲取一個新的、獨立的
Connection
。 - 這個新
Connection
不參與原始事務,其操作會在自身autoCommit
模式下立即執行(通常是自動提交),與原始事務完全隔離。
新線程中的數據庫操作成功與否不影響原始事務的提交或回滾,反之亦然。破壞了事務的原子性(Atomicity)。原始事務回滾不會回滾新線程中的操作;新線程操作失敗也不會導致原始事務回滾。
解決方案:處理多線程下的數據一致性非常復雜,沒有銀彈:
- **避免在事務方法內開啟異步線程執行 DB 操作:**這是最根本的預防措施。將需要在同一事務中完成的操作放在同一個線程內執行。
- 編程式事務管理: 在新線程內部,使用
TransactionTemplate
手動管理事務邊界。但這只是讓新線程內部操作具有事務性,無法與原始線程的事務合并成一個原子事務。 - **分布式事務:**如果業務強要求跨線程的 ACID,可能需要引入分布式事務管理器(如 Seata, Atomikos)來處理這種跨 資源(不同線程可視為不同資源管理者)的場景,但代價高昂且復雜。
- 設計補償機制: 在業務層設計最終一致性方案(如 Saga 模式),通過記錄操作日志、發送消息、定時任務補償等方式,在異步操作失敗后嘗試回滾或修正原始事務已提交的操作。這是更常見的處理異步事務一致性的實踐。
五、錯誤傳播行為(如:PROPAGATION_NOT_SUPPORTED掛起事務)
@Transactional
的 propagation
屬性定義了當前方法的事務如何與已存在的事務進行交互。使用不當會導致事務行為不符合預期。
PROPAGATION_NOT_SUPPORTED
: 不支持事務。如果當前存在事務,則掛起(Suspend) 這個事務;然后以非事務方式執行當前方法。方法執行完畢后,之前掛起的事務恢復(Resume)。
假設方法 outer()
開啟了一個事務(Propagation.REQUIRED
),在其內部調用 inner()
方法,而 inner()
被標注為 @Transactional(propagation = Propagation.NOT_SUPPORTED)
,當執行到 inner()
時:
- 系統檢測到當前存在
outer()
開啟的事務。 - 根據
NOT_SUPPORTED
語義,掛起outer()
的事務。 inner()
方法在無事務狀態下執行(相當于autoCommit=true
)。inner()
方法執行完畢(無論成功失敗,其操作已立即提交)。- 恢復
outer()
的事務,繼續執行outer()
剩余代碼。
結果是 inner()
方法中的數據庫操作不受 outer()
事務控制。即使 outer()
最終因異常回滾,inner()
中已提交的操作不會被回滾!這通常不是開發者想要的效果,極易造成數據不一致。
其他易錯傳播行為:
PROPAGATION_NEVER
: 要求不能存在事務。如果調用者在一個事務中調用了標記為NEVER
的方法,會直接拋出IllegalTransactionStateException
異常。PROPAGATION_SUPPORTS
: 如果當前存在事務,就加入該事務;如果沒有,就以非事務方式執行。關鍵點在于非事務方式。如果方法中有多個操作且需要原子性,而外部又恰好沒有事務,這些操作就會各自獨立提交。PROPAGATION_REQUIRES_NEW
: 總是開啟一個全新的、獨立的事務。會掛起外部事務(如果存在)。新事務的提交/回滾與外部事務互不影響。注意: 這雖然創建了新事務,但不同于自調用失效,它是有效的(通過代理調用)。它的陷阱在于開發者可能誤以為新事務是外部事務的一部分,其實它們是獨立的。
解決方案:
- 深入理解傳播行為: 務必清楚每種傳播行為(
REQUIRED
,REQUIRES_NEW
,SUPPORTS
,MANDATORY
,NOT_SUPPORTED
,NEVER
,NESTED
)的精確語義。 - 謹慎選擇傳播行為: 默認使用
Propagation.REQUIRED
通常能滿足大多數場景(加入現有事務,沒有則新建)。只有在有明確且充分理由時才使用其他傳播行為。 - 代碼審查與測試: 對使用了非默認傳播行為的代碼進行重點審查,并通過單元測試、集成測試模擬各種調用鏈路,驗證事務邊界和回滾行為是否符合預期。特別注意跨方法、跨服務調用時的事務傳播。
六、總結
Spring Boot 事務失效的核心原因通常圍繞:
- AOP 代理機制的限制(非 public、自調用)
- 異常處理機制(默認回滾異常類型)
- 資源綁定機制(ThreadLocal 導致多線程失效)
- 配置錯誤(傳播行為誤用)
解決這些問題需要深入理解 Spring 事務管理的底層原理(代理、ThreadLocal、異常回滾規則、傳播語義),并在編碼和配置時保持謹慎,遵循最佳實踐(如方法 public、避免自調用、顯式指定 rollbackFor、理解傳播行為、避免事務內跨線程操作 DB)。