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