炸裂!@Transactional遇上@Async:是"王炸組合"還是"致命事故"
環(huán)境:SpringBoot3.4.2
1. 簡介
在Spring Boot開發(fā)中,@Transactional和@Async是兩個高頻使用的注解,分別服務(wù)于不同的場景。
@Transactional 是Spring事務(wù)管理的核心注解,用于聲明方法需要在事務(wù)上下文中執(zhí)行,確保數(shù)據(jù)庫操作的原子性(ACID)。它通過AOP代理實現(xiàn),默認(rèn)基于數(shù)據(jù)庫連接的事務(wù)傳播機制(如PROPAGATION_REQUIRED)。
@Async 是Spring異步任務(wù)的核心注解,用于將方法標(biāo)記為異步執(zhí)行,本質(zhì)是通過線程池啟動新線程處理任務(wù),避免阻塞主線程,提升系統(tǒng)吞吐量。
當(dāng)我們寫出如下的代碼后是否存在問題呢?
@Transactional
@Async
public void processProduct(Product product) {
this.productRepository.saveAndFlush(product) ;
this.emailService.send() ;
}
@Service
public class EmailService {
public void send() {
System.err.printf("%s - 發(fā)送郵件", Thread.currentThread().getName()) ;
System.err.println(1 / 0) ;
}
}
EmailService#send方法中我們模擬了異常拋出,執(zhí)行上面的processProduct方法事務(wù)是正常執(zhí)行還是回滾呢?
2.問題復(fù)現(xiàn)
2.1 單元測試
通過如下單元測試:
@Resource
private ProductService productService ;
@Test
public void testCreateProduct() {
Product product = new Product("Spring全家桶實戰(zhàn)案例源碼", 70D);
this.productService.processProduct(product) ;
}
數(shù)據(jù)庫初始狀態(tài)如下:
圖片
執(zhí)行結(jié)果
圖片
程序拋出異常后,數(shù)據(jù)庫中未插入任何數(shù)據(jù),此結(jié)果完全符合預(yù)期要求。
這一現(xiàn)象表明,在默認(rèn)配置條件下,@Transactional 與 @Async 這兩個注解能夠?qū)崿F(xiàn)良好的協(xié)同運作,共同達成預(yù)期的業(yè)務(wù)邏輯處理效果。
2.2 錯誤情況
我們知道@Transactional 與 @Async 2個注解底層的實現(xiàn)都是通過AOP實現(xiàn)的,那么接下來,我們進行如下的配置修改:
@Configuration
@EnableAsync(order = Ordered.HIGHEST_PRECEDENCE)
public class AsyncConfig {
}
將@EnableAsync注解的order屬性設(shè)置為最高優(yōu)先級(值越小,優(yōu)先級越高)。再次運行上面的測試程序,執(zhí)行結(jié)果如下(先將數(shù)據(jù)庫中的數(shù)據(jù)清空):
錯誤還是一樣的錯誤。
圖片
但是數(shù)據(jù)庫中成功插入了數(shù)據(jù),也就是事務(wù)沒有回滾。
3. 原因分析
在上述場景中已明確,默認(rèn)情況下 @Transactional 與 @Async 可正常協(xié)同運作,若調(diào)整 @EnableAsync 的 order 屬性,事務(wù)會失效。
接下來,我們將進行底層原理的分析。
3.1 代理創(chuàng)建的原理
當(dāng)項目中引入spring-boot-starter-aop時,會自動通過@EnableAspectJAutoProxy注解開啟代理功能,其實就是注冊了一個BeanPostProcessor處理器:AnnotationAwareAspectJAutoProxyCreator。
有了處理器后還需要切面,而在Spring中定義切面的方式有2種:
- 使用 @Aspect 聲明的高級切面
通過該注解聲明的切面最終會被轉(zhuǎn)換為低級切面Advisor。 - 通過實現(xiàn) Advisor 接口實現(xiàn)低級切面
總結(jié):代理對象的創(chuàng)建是通過BeanPostProcessor+Advisor實現(xiàn)。
3.2 @Transactional底層實現(xiàn)
當(dāng)我們項目中引入相關(guān)數(shù)據(jù)庫操作的starter時,如:spring-boot-starter-data-jpa或者spring-boot-starter-data-jdbc。底層的自動配置會通過@EnableTransactionManagement注解開啟@Transactional注解的的事務(wù)功能。
而@EnableTransactionManagement注解會自動的注冊,BeanFactoryTransactionAttributeSourceAdvisor切面。同時還會注冊InfrastructureAdvisorAutoProxyCreator處理器,但是AnnotationAwareAspectJAutoProxyCreator處理器的優(yōu)先級高于InfrastructureAdvisorAutoProxyCreator,所以最終底層最終使用的BeanPostProcessor處理將是AnnotationAwareAspectJAutoProxyCreator。
總結(jié):@Transactional事務(wù)注解將通過AnnotationAwareAspectJAutoProxyCreator + BeanFactoryTransactionAttributeSourceAdvisor創(chuàng)建代理對象。
3.3 @Async底層原理
要使用異步功能,我們需要通過@EnableAsync開啟功能,而該注解會自動注冊:AsyncAnnotationBeanPostProcessor處理器,而切面則是AsyncAnnotationAdvisor。
但是該處理器會先判斷當(dāng)前的類是不是已經(jīng)是代理對象了,如果是則只是將AsyncAnnotationAdvisor添加到當(dāng)前的切面集合中,如下源碼:
圖片
那這時候是不是就是看處理@Async和@Transactional注解的處理器BeanPostProcessor誰先執(zhí)行了?!
3.4 處理器執(zhí)行順序
AnnotationAwareAspectJAutoProxyCreator處理器默認(rèn)注冊的時候設(shè)置的優(yōu)先級是最高優(yōu)先級,如下源碼:
圖片
默認(rèn)情況,通過debug查看執(zhí)行順序
圖片
通過這樣的執(zhí)行順序,處理@Async異步任務(wù)時,這將會先開啟一個異步線程,那么后續(xù)的攔截器再執(zhí)行的時候都將會在這個異步線程中,那么這樣也就保證了事務(wù)的正確性。
當(dāng)我們通過@EnableAsync(order = Ordered.HIGHEST_PRECEDENCE)調(diào)整順序后,查看BeanPostProcessor執(zhí)行順序:
圖片
當(dāng)處理 @Async 注解的處理器先執(zhí)行時,會為對應(yīng) Bean 創(chuàng)建代理對象。待處理 @Transactional 注解的處理器執(zhí)行時,因?qū)ο笠褳榇?,會基于原始類再生成代理(?targetSource 指向 @Async 代理對象)。最終執(zhí)行業(yè)務(wù)代碼時,@Transactional 代理先觸發(fā)切面邏輯(開啟事務(wù)于主線程),隨后 @Async 代理開啟異步線程。由于事務(wù)與業(yè)務(wù)操作分屬不同線程,事務(wù)無法隨異常觸發(fā)回滾。
總結(jié):默認(rèn)你不調(diào)整執(zhí)行順序那么@Transactional+@Async能很好的協(xié)同工作。
4. 新特性
從Spring 6.2起,@EnableTransactionManagement增加了一個屬性配置,可以全局控制異?;貪L策略,不用再每一個@Transactional注解上進行配置回滾策略了。
@Configuration
@EnableTransactionManagement(
rollbackOn = RollbackOn.ALL_EXCEPTIONS)
public class TxConfig {
}
RollbackOn支持2種類型,如下:
public enum RollbackOn {
RUNTIME_EXCEPTIONS,
ALL_EXCEPTIONS
}