SpringBoot @Async:魔法和陷阱
來(lái)源:https://medium.com/
?? 歡迎加入小哈的星球 ,你將獲得: 專(zhuān)屬的項(xiàng)目實(shí)戰(zhàn)/ Java 學(xué)習(xí)路線 / 一對(duì)一提問(wèn) / 學(xué)習(xí)打卡/贈(zèng)書(shū)福利
目前, 正在星球內(nèi)部帶小伙伴做第一個(gè)項(xiàng)目:全棧前后端分離博客,手摸手,后端 + 前端全棧開(kāi)發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開(kāi)發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了125小節(jié),累計(jì)20w+字,講解圖:805張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有410+小伙伴加入(早鳥(niǎo)價(jià)超低)
@Async注解就像是springboot項(xiàng)目中性能優(yōu)化的秘密武器。是的,我們也可以手動(dòng)創(chuàng)建自己的執(zhí)行器和線程池,但@Async使事情變得更簡(jiǎn)單、更神奇。
@Async注釋 允許我們?cè)诤笈_(tái)運(yùn)行代碼,因此我們的主線程可以繼續(xù)運(yùn)行,而無(wú)需等待較慢的任務(wù)完成。但是,就像所有秘密武器一樣,明智地使用它并了解它的局限性非常重要。
在這篇文章中,我們將深入探討@Async 的魔力以及在 Spring Boot 項(xiàng)目中使用它時(shí)應(yīng)該注意的問(wèn)題。讓我們開(kāi)始吧!
首先讓我們學(xué)習(xí)如何在應(yīng)用程序中使用 @Async 的基礎(chǔ)知識(shí)。
我們需要在 Spring Boot 應(yīng)用程序中啟用@Async 。為此,我們需要將@EnableAsync注釋添加到配置類(lèi)或主應(yīng)用程序文件中。這將為應(yīng)用程序中使用@Async注釋的所有方法啟用異步行為。
@SpringBootApplication
@EnableAsync
public class BackendAsjApplication {
}
我們還需要?jiǎng)?chuàng)建一個(gè) Bean,指定使用 @Async 注釋的方法的配置。我們可以設(shè)置最大線程池大小、隊(duì)列大小等。不過(guò),添加這些配置時(shí)要小心。否則,我們可能很快就會(huì)耗盡內(nèi)存。我通常還會(huì)添加一個(gè)日志,以在隊(duì)列大小已滿并且沒(méi)有更多線程來(lái)接收新傳入任務(wù)時(shí)發(fā)出警告。
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("MyAsyncThread-");
executor.setRejectedExecutionHandler((r, executor1) -> log.warn("Task rejected, thread pool is full and queue is also full"));
executor.initialize();
return executor;
}
現(xiàn)在,讓我們使用它。假設(shè)我們有一個(gè)服務(wù)類(lèi),其中包含我們想要異步的方法。我們將使用@Async注釋此方法。
@Service
public class EmailService {
@Async
public void sendEmail() {
}
}
在代碼示例中,您會(huì)看到多次提到EmailService和PurchaseService。這些只是示例。我不想將所有內(nèi)容都命名為“MyService”。因此,將其命名為更有意義的名稱(chēng)。在電子商務(wù)應(yīng)用程序中,您當(dāng)然希望您的 EmailService 是異步的,這樣客戶請(qǐng)求就不會(huì)被阻止
現(xiàn)在,當(dāng)我們調(diào)用此方法時(shí),它將立即返回,從而釋放調(diào)用線程(通常是主線程)以繼續(xù)執(zhí)行其他任務(wù)。該方法將繼續(xù)在后臺(tái)執(zhí)行,稍后將結(jié)果返回給調(diào)用線程。由于我們?cè)谶@里用 void 標(biāo)記了 @Async 方法,因此我們對(duì)它何時(shí)完成并不真正感興趣。
非常簡(jiǎn)單而且非常強(qiáng)大,對(duì)吧?(當(dāng)然,我們可以做更多配置,但上面的代碼足以運(yùn)行完全異步的任務(wù))
但是,在我們開(kāi)始使用 @Async 注釋所有方法之前,我們需要注意一些問(wèn)題。
1@Async方法需要位于不同的類(lèi)中
使用 @Async 注釋時(shí),請(qǐng)務(wù)必注意,我們不能從同一類(lèi)中調(diào)用 @Async 方法。這是因?yàn)檫@樣做會(huì)導(dǎo)致無(wú)限循環(huán)并導(dǎo)致應(yīng)用程序掛起。
以下是不應(yīng)該做的事情的示例:
@Service
public class PurchaseService {
public void purchase(){
sendEmail();
}
@Async
public void sendEmail(){
// Asynchronous code
}
}
相反,我們應(yīng)該為異步方法使用單獨(dú)的類(lèi)或服務(wù)。
@Service
public class EmailService {
@Async
public void sendEmail(){
// Asynchronous code
}
}
@Service
public class PurchaseService {
public void purchase(){
emailService.sendEmail();
}
@Autowired
private EmailService emailService;
}
現(xiàn)在您可能想知道,我可以從另一個(gè)異步方法中調(diào)用異步方法嗎?最簡(jiǎn)潔的答案是不。當(dāng)調(diào)用異步方法時(shí),它會(huì)在不同的線程中執(zhí)行,并且調(diào)用線程會(huì)繼續(xù)執(zhí)行下一個(gè)任務(wù)。如果調(diào)用線程本身是異步方法,則它無(wú)法等待被調(diào)用的異步方法完成后再繼續(xù),這可能會(huì)導(dǎo)致意外行為。
2@Async 和 @Transcational 配合不佳
@Transactional 注釋用于指示方法或類(lèi)應(yīng)該參與事務(wù)。它用于確保一組數(shù)據(jù)庫(kù)操作作為單個(gè)工作單元執(zhí)行,并且在發(fā)生任何故障時(shí)數(shù)據(jù)庫(kù)保持一致?tīng)顟B(tài)。
當(dāng)一個(gè)方法被@Transactional注解時(shí),Spring會(huì)在該方法周?chē)鷦?chuàng)建一個(gè)代理,并且該方法內(nèi)的所有數(shù)據(jù)庫(kù)操作都在事務(wù)上下文中執(zhí)行。Spring 還負(fù)責(zé)在調(diào)用方法之前啟動(dòng)事務(wù),并在方法返回后提交事務(wù),或者在發(fā)生異常時(shí)回滾事務(wù)。
但是,當(dāng)您使用 @Async 注釋使方法異步時(shí),該方法將在與主應(yīng)用程序線程不同的單獨(dú)線程中執(zhí)行。這意味著該方法不再在 Spring 啟動(dòng)的事務(wù)上下文中執(zhí)行。因此,@Async方法內(nèi)的數(shù)據(jù)庫(kù)操作不會(huì)參與事務(wù),并且在出現(xiàn)異常時(shí)數(shù)據(jù)庫(kù)可能會(huì)處于不一致的狀態(tài)。
@Service
public class EmailService {
@Transactional
public void transactionalMethod() {
//database operation 1
asyncMethod();
//database operation 2
}
@Async
public void asyncMethod() {
//database operation 3
}
}
在此示例中,數(shù)據(jù)庫(kù)操作 1 和數(shù)據(jù)庫(kù)操作 2 在 Spring 啟動(dòng)的事務(wù)上下文中執(zhí)行。但是,數(shù)據(jù)庫(kù)操作 3 是在單獨(dú)的線程中執(zhí)行的,并且不是事務(wù)的一部分。
因此,如果在執(zhí)行數(shù)據(jù)庫(kù)操作3之前發(fā)生異常,則數(shù)據(jù)庫(kù)操作1和數(shù)據(jù)庫(kù)操作2將按預(yù)期回滾,但數(shù)據(jù)庫(kù)操作3不會(huì)回滾。這可能會(huì)使數(shù)據(jù)庫(kù)處于不一致的狀態(tài)。
當(dāng)然,有很多方法可以解決這個(gè)問(wèn)題,即使用 TransactionTemplate 之類(lèi)的東西來(lái)管理事務(wù),但開(kāi)箱即用,如果從轉(zhuǎn)換方法調(diào)用異步方法,最終會(huì)出現(xiàn)問(wèn)題。
3@Async 阻塞問(wèn)題
假設(shè)這是我們的 @Async 線程池的配置:
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("MyAsyncThread-");
executor.setRejectedExecutionHandler((r, executor1) -> log.warn("Task rejected, thread pool is full and queue is also full"));
executor.initialize();
return executor;
}
這意味著在任何特定時(shí)刻,我們最多將運(yùn)行 2 個(gè) @Async 任務(wù)。如果有更多任務(wù)進(jìn)來(lái),它們將排隊(duì),直到隊(duì)列大小達(dá)到 500。
但現(xiàn)在假設(shè),我們的 @Async 任務(wù)之一執(zhí)行起來(lái)花費(fèi)了太多時(shí)間,或者只是由于外部依賴(lài)而被阻止。這意味著所有其他任務(wù)將排隊(duì)并且執(zhí)行速度不夠快。根據(jù)您的應(yīng)用程序類(lèi)型,這可能會(huì)導(dǎo)致延遲。
解決此問(wèn)題的一種方法是為長(zhǎng)時(shí)間運(yùn)行的任務(wù)使用單獨(dú)的線程池,為更緊急且不需要大量處理時(shí)間的任務(wù)使用單獨(dú)的線程池。我們可以這樣做:
@Primary
@Bean(name = "taskExecutorDefault")
public ThreadPoolTaskExecutor taskExecutorDefault() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("Async-1-");
executor.initialize();
return executor;
}
@Bean(name = "taskExecutorForHeavyTasks")
public ThreadPoolTaskExecutor taskExecutorRegistration() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("Async2-");
executor.initialize();
return executor;
}
然后要使用它,只需在 @Async 聲明中添加執(zhí)行器的名稱(chēng)即可:
@Service
public class EmailService {
@Async("taskExecutorForHeavyTasks")
public void sendEmailHeavy() {
//method implementation
}
}
但是,請(qǐng)注意,我們不應(yīng)該在調(diào)用Thread.sleep()或的方法上使用@Async Object.wait(),因?yàn)樗鼤?huì)阻塞線程,并且使用@Async的目的將落空。
4@Async 中的異常
另一件需要記住的事情是 @Async 方法不會(huì)向調(diào)用線程拋出異常。這意味著您需要在 @Async 方法中正確處理異常,否則它們將丟失。
以下是不應(yīng)該做的事情的示例:
@Service
public class EmailService {
@Async
public void sendEmail() throws Exception{
throw new Exception("Oops, cannot send email!");
}
}
@Service
public class PurchaseService {
@Autowired
private EmailService emailService;
public void purchase(){
try{
emailService.sendEmail();
}catch (Exception e){
System.out.println("Caught exception: " + e.getMessage());
}
}
}
在上面的代碼中,異常在 asyncMethod() 中拋出,但不會(huì)被調(diào)用線程捕獲,并且 catch 塊不會(huì)被執(zhí)行。
為了正確處理 @Async 方法中的異常,我們可以結(jié)合使用 Future 和 try-catch 塊。這是一個(gè)例子:
@Service
public class EmailService {
@Async
public Future<String> sendEmail() throws Exception{
throw new Exception("Oops, cannot send email!");
}
}
@Service
public class PurchaseService {
@Autowired
private EmailService emailService;
public void purchase(){
try{
Future<String> future = emailService.sendEmail();
String result = future.get();
System.out.println("Result: " + result);
}catch (Exception e){
System.out.println("Caught exception: " + e.getMessage());
}
}
}
通過(guò)返回 Future 對(duì)象并使用 try-catch 塊,我們可以正確處理和捕獲 @Async 方法中引發(fā)的異常。
總之,Spring Boot中的@Async注釋是提高應(yīng)用程序性能和可伸縮性的強(qiáng)大工具。但是,小心使用它并注意它的局限性是很重要的。通過(guò)理解這些陷阱并使用CompletableFuture和Executor等技術(shù),您可以充分利用@Async注釋并將應(yīng)用程序提升到下一個(gè)級(jí)別。
?? 歡迎加入小哈的星球 ,你將獲得: 專(zhuān)屬的項(xiàng)目實(shí)戰(zhàn)/ Java 學(xué)習(xí)路線 / 一對(duì)一提問(wèn) / 學(xué)習(xí)打卡/贈(zèng)書(shū)福利
目前, 正在星球內(nèi)部帶小伙伴做第一個(gè)項(xiàng)目:全棧前后端分離博客,手摸手,后端 + 前端全棧開(kāi)發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開(kāi)發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了125小節(jié),累計(jì)20w+字,講解圖:805張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有410+小伙伴加入(早鳥(niǎo)價(jià)超低)
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號(hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過(guò)內(nèi)容,記得讀完點(diǎn)一下“在看”,加個(gè)“星標(biāo)”,這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。
點(diǎn)“在看”支持小哈呀,謝謝啦
