SpringBoot @Async:魔法和陷阱
共 10772字,需浏览 22分钟
·
2023-11-09 00:14
来源:https://medium.com/
👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战/ Java 学习路线 / 一对一提问 / 学习打卡/赠书福利
目前, 正在星球内部带小伙伴做第一个项目:全栈前后端分离博客,手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了125小节,累计20w+字,讲解图:805张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有410+小伙伴加入(早鸟价超低)
@Async注解就像是springboot项目中性能优化的秘密武器。是的,我们也可以手动创建自己的执行器和线程池,但@Async使事情变得更简单、更神奇。
@Async注释 允许我们在后台运行代码,因此我们的主线程可以继续运行,而无需等待较慢的任务完成。但是,就像所有秘密武器一样,明智地使用它并了解它的局限性非常重要。
在这篇文章中,我们将深入探讨@Async 的魔力以及在 Spring Boot 项目中使用它时应该注意的问题。让我们开始吧!
首先让我们学习如何在应用程序中使用 @Async 的基础知识。
我们需要在 Spring Boot 应用程序中启用@Async 。为此,我们需要将@EnableAsync注释添加到配置类或主应用程序文件中。这将为应用程序中使用@Async注释的所有方法启用异步行为。
@SpringBootApplication
@EnableAsync
public class BackendAsjApplication {
}
我们还需要创建一个 Bean,指定使用 @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;
}
现在,让我们使用它。假设我们有一个服务类,其中包含我们想要异步的方法。我们将使用@Async注释此方法。
@Service
public class EmailService {
@Async
public void sendEmail() {
}
}
在代码示例中,您会看到多次提到EmailService和PurchaseService。这些只是示例。我不想将所有内容都命名为“MyService”。因此,将其命名为更有意义的名称。在电子商务应用程序中,您当然希望您的 EmailService 是异步的,这样客户请求就不会被阻止
现在,当我们调用此方法时,它将立即返回,从而释放调用线程(通常是主线程)以继续执行其他任务。该方法将继续在后台执行,稍后将结果返回给调用线程。由于我们在这里用 void 标记了 @Async 方法,因此我们对它何时完成并不真正感兴趣。
非常简单而且非常强大,对吧?(当然,我们可以做更多配置,但上面的代码足以运行完全异步的任务)
但是,在我们开始使用 @Async 注释所有方法之前,我们需要注意一些问题。
1@Async方法需要位于不同的类中
使用 @Async 注释时,请务必注意,我们不能从同一类中调用 @Async 方法。这是因为这样做会导致无限循环并导致应用程序挂起。
以下是不应该做的事情的示例:
@Service
public class PurchaseService {
public void purchase(){
sendEmail();
}
@Async
public void sendEmail(){
// Asynchronous code
}
}
相反,我们应该为异步方法使用单独的类或服务。
@Service
public class EmailService {
@Async
public void sendEmail(){
// Asynchronous code
}
}
@Service
public class PurchaseService {
public void purchase(){
emailService.sendEmail();
}
@Autowired
private EmailService emailService;
}
现在您可能想知道,我可以从另一个异步方法中调用异步方法吗?最简洁的答案是不。当调用异步方法时,它会在不同的线程中执行,并且调用线程会继续执行下一个任务。如果调用线程本身是异步方法,则它无法等待被调用的异步方法完成后再继续,这可能会导致意外行为。
2@Async 和 @Transcational 配合不佳
@Transactional 注释用于指示方法或类应该参与事务。它用于确保一组数据库操作作为单个工作单元执行,并且在发生任何故障时数据库保持一致状态。
当一个方法被@Transactional注解时,Spring会在该方法周围创建一个代理,并且该方法内的所有数据库操作都在事务上下文中执行。Spring 还负责在调用方法之前启动事务,并在方法返回后提交事务,或者在发生异常时回滚事务。
但是,当您使用 @Async 注释使方法异步时,该方法将在与主应用程序线程不同的单独线程中执行。这意味着该方法不再在 Spring 启动的事务上下文中执行。因此,@Async方法内的数据库操作不会参与事务,并且在出现异常时数据库可能会处于不一致的状态。
@Service
public class EmailService {
@Transactional
public void transactionalMethod() {
//database operation 1
asyncMethod();
//database operation 2
}
@Async
public void asyncMethod() {
//database operation 3
}
}
在此示例中,数据库操作 1 和数据库操作 2 在 Spring 启动的事务上下文中执行。但是,数据库操作 3 是在单独的线程中执行的,并且不是事务的一部分。
因此,如果在执行数据库操作3之前发生异常,则数据库操作1和数据库操作2将按预期回滚,但数据库操作3不会回滚。这可能会使数据库处于不一致的状态。
当然,有很多方法可以解决这个问题,即使用 TransactionTemplate 之类的东西来管理事务,但开箱即用,如果从转换方法调用异步方法,最终会出现问题。
3@Async 阻塞问题
假设这是我们的 @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;
}
这意味着在任何特定时刻,我们最多将运行 2 个 @Async 任务。如果有更多任务进来,它们将排队,直到队列大小达到 500。
但现在假设,我们的 @Async 任务之一执行起来花费了太多时间,或者只是由于外部依赖而被阻止。这意味着所有其他任务将排队并且执行速度不够快。根据您的应用程序类型,这可能会导致延迟。
解决此问题的一种方法是为长时间运行的任务使用单独的线程池,为更紧急且不需要大量处理时间的任务使用单独的线程池。我们可以这样做:
@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 声明中添加执行器的名称即可:
@Service
public class EmailService {
@Async("taskExecutorForHeavyTasks")
public void sendEmailHeavy() {
//method implementation
}
}
但是,请注意,我们不应该在调用Thread.sleep()
或的方法上使用@Async Object.wait()
,因为它会阻塞线程,并且使用@Async的目的将落空。
4@Async 中的异常
另一件需要记住的事情是 @Async 方法不会向调用线程抛出异常。这意味着您需要在 @Async 方法中正确处理异常,否则它们将丢失。
以下是不应该做的事情的示例:
@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()
中抛出,但不会被调用线程捕获,并且 catch 块不会被执行。
为了正确处理 @Async 方法中的异常,我们可以结合使用 Future 和 try-catch 块。这是一个例子:
@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());
}
}
}
通过返回 Future 对象并使用 try-catch 块,我们可以正确处理和捕获 @Async 方法中引发的异常。
总之,Spring Boot中的@Async注释是提高应用程序性能和可伸缩性的强大工具。但是,小心使用它并注意它的局限性是很重要的。通过理解这些陷阱并使用CompletableFuture和Executor等技术,您可以充分利用@Async注释并将应用程序提升到下一个级别。
👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战/ Java 学习路线 / 一对一提问 / 学习打卡/赠书福利
目前, 正在星球内部带小伙伴做第一个项目:全栈前后端分离博客,手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了125小节,累计20w+字,讲解图:805张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有410+小伙伴加入(早鸟价超低)
1. 我的私密学习小圈子~
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦