在 Spring 框架中,@Transactional
註解作為一種宣告式事務管理的關鍵機制,其背後的工作原理遠比簡單的 AOP
(面向切面程式設計)和 ThreadLocal
儲存更為細膩。該註解的實現核心在於 Spring
的 TransactionInterceptor
(事務攔截器)以及它如何與 Spring
的代理機制、TransactionManager
(事務管理器)協同工作,來確保事務的開啟、提交或回滾等操作得以正確執行。
一 註解解析與代理生成
當 Spring 容器初始化時,會透過 AnnotationTransactionAttributeSource 掃描並識別出所有標有 @Transactional 的方法。這些方法在被呼叫前,Spring 會根據配置(如基於介面或類的代理)為它們建立動態代理物件。如果是基於介面的代理,則使用 JDK Dynamic Proxy;如果是基於類的,則採用 CGLIB。這個代理物件會在目標方法呼叫前後插入事務處理邏輯。
1.1 Spring Bean的後處理器
一切始於 Spring 容器的 Bean 建立和初始化過程。Spring 透過一系列的 BeanPostProcessor 介面實現類來增強 Bean 的功能,其中與事務管理密切相關的便是 AbstractAutoProxyCreator
的子類,如 AnnotationAwareAspectJAutoProxyCreator
。這個類負責掃描並建立代理物件,以便於在執行時織入諸如事務管理這樣的切面邏輯。
1.2 識別 @Transactional 註解
ClassPathScanningCandidateComponentProvider:Spring 首先會使用此類掃描指定包路徑下帶有特定註解(如
@Transactional
)的類或方法。AnnotationTransactionAttributeSource:一旦找到帶有
@Transactional
註解的類或方法,Spring 會使用AnnotationTransactionAttributeSource
來解析這些註解,將其轉換為事務屬性(TransactionAttribute),比如事務的隔離級別、傳播行為、超時時間等。
1.3 建立代理物件
對於基於介面的代理,Spring 使用 JDK 的動態代理技術,透過
JdkDynamicAopProxy
建立代理物件。該代理會檢查呼叫鏈,並在呼叫目標方法前插入事務管理的前置邏輯,呼叫後插入後置邏輯(如提交或回滾事務)。對於沒有實現介面的類,Spring 則利用 CGLIB 庫生成目標類的子類作為代理,透過
CglibAopProxy
來完成。CGLIB 代理同樣能夠在目標方法呼叫的前後插入事務管理程式碼。
1.4 TransactionInterceptor
在代理物件中,事務管理的具體邏輯是由
TransactionInterceptor
(事務攔截器)來執行的。它實現了MethodInterceptor
介面,因此當代理物件的方法被呼叫時,會進入invoke(MethodInvocation invocation)
方法。在這個方法內,TransactionInterceptor
根據解析出的事務屬性來決定是否開啟事務、使用何種傳播行為,並最終呼叫目標方法。
總結下就是,Spring 透過 Bean 的後置處理器在容器初始化階段自動檢測帶有 @Transactional
註解的類和方法,並透過動態代理機制為這些元件建立代理物件。代理物件在方法呼叫時,透過 TransactionInterceptor
這一核心元件,根據註解配置的事務屬性來管理事務生命週期,確保事務邏輯的無縫整合。
二 事務攔截與執行
2.1 TransactionInterceptor 的作用
TransactionInterceptor
實現了 Spring 的 MethodInterceptor
介面,這意味著它能夠攔截方法呼叫,並在呼叫前後執行額外的邏輯,即事務管理邏輯。其核心職責包括:
識別是否需要事務: 根據目標方法上的
@Transactional
註解(如果有的話)及其屬性(如傳播行為、隔離級別、超時時間等),決定是否需要啟動一個新的事務或加入到現有的事務中。需要注意的是,事務上下文(包括連線資訊等)被儲存在由 Spring 管理的一個特定的 TransactionSynchronizationManager 中,TransactionSynchronizationManager 內部使用了 ThreadLocal。事務管理: 在方法呼叫之前開啟事務,呼叫之後根據方法執行結果提交或回滾事務。這包括異常處理邏輯,即在遇到未被捕獲的異常時,確保事務被正確回滾。
2.2 攔截與執行流程
方法呼叫前:
事務屬性解析:
TransactionInterceptor
首先透過TransactionAttributeSource
(通常是AnnotationTransactionAttributeSource
)獲取目標方法的事務屬性。事務開始: 根據解析出的事務屬性,決定是否建立新的事務或者加入到當前事務中。如果需要新事務,它會透過事務管理器(如
PlatformTransactionManager
的實現類)來開始事務。方法執行:
在事務上下文中執行實際的目標方法。此時,如果目標方法內部丟擲了異常,這個異常會被暫存以供後續處理。
方法呼叫後:
異常處理: 如果方法執行過程中丟擲了異常,
TransactionInterceptor
會捕獲到這個異常,並根據異常型別及事務屬性決定是否需要回滾事務。通常,非檢查型異常(繼承自RuntimeException
的異常)會導致事務回滾,而檢查型異常則不會,除非事務屬性特別指定了回滾規則。事務提交或回滾: 如果方法正常結束,或者按事務屬性應該提交事務的情況下,事務管理器會提交事務。相反,如果需要回滾,則執行回滾操作。
資源清理: 在事務結束後,確保所有資源被正確釋放,比如關閉資料庫連線等。
2.3 動態代理的作用
整個過程中,動態代理扮演著關鍵角色。無論是 JDK 動態代理還是 CGLIB 代理,它們都是在呼叫真正業務方法之前插入事務攔截邏輯的橋樑。當客戶端程式碼呼叫代理物件上的方法時,實際上是呼叫了由 TransactionInterceptor
所控制的代理邏輯,從而透明地在業務邏輯執行前後管理事務。
透過這種方式,開發者無需在每個需要事務管理的方法中手動編寫開啟、提交或回滾事務的程式碼,極大地簡化了開發複雜度,提高了程式碼的可維護性和可讀性。
三 多執行緒環境下的挑戰
當 @Transactional 標記的方法內部啟動新的執行緒時,問題就顯現了。前面提到,事務攔截使用了 TransactionInterceptor,而 TransactionInterceptor 內部用到了 TransactionSynchronizationManager,TransactionSynchronizationManager 使用 ThreadLocal 來儲存事務相關的資源資訊,如資料庫連線、JMS 會話等。這意味著每個執行緒都有其獨立的事務上下文,確保了事務資訊線上程間的隔離。
換句話說,Spring 管理的事務上下文是基於呼叫執行緒的,新執行緒並沒有繼承原執行緒的 TransactionSynchronizationManager 中的事務上下文。因此,新執行緒執行的資料庫操作實際上是在無事務管理的環境下進行的,這就導致事務失效。
那如果非要用多執行緒怎麼辦呢?這個時候可以使用程式設計式事務,首先設定一個全域性變數 Boolean,預設是可以提交的 true,在子執行緒,透過程式設計式事務的方式去開啟事務,然後插入資料,插入完成後,事務先不提交,同時通知主執行緒,我準備好了,進入等待狀態。如果子執行緒出現異常,那就通知主執行緒,我這邊發生異常,然後自己回滾。
最後主執行緒收集各個子執行緒的狀態,如果有一個執行緒出現問題,那麼全域性變數就設定為不可提交false,然後喚醒所有子執行緒,進行回滾。
這裏涉及到執行緒同步可以利用閉鎖去實現;當主執行緒通知各個子執行緒提交事務的時候,子執行緒可能在提交的時候出錯了,最終導致資料不一致,那麼這種時候可能就需要引入重試機制,有了重試機制後,又要去保證冪等性等等。
這一套方案下來大夥有沒有覺得眼熟,是不是就是分散式事務的處理思路了?
所以說,非要在事務中開啟多執行緒也沒問題,但是不建議這麼做。