SpringBoot如何实现异步编程?大师们就是这样做的!
今天,我们来讨论一下如何在SpringBoot项目中实现异步编程。首先,让我们看看为什么在Spring中使用异步编程以及它可以解决什么问题。
为什么使用异步框架,它解决了什么问题?
在SpringBoot的日常开发中,通常会使用同步调用。然而,在现实中,有许多场景非常适合异步处理。
例如,当注册新用户时,将发送电子邮件提醒。之后,当你升级为会员时,你将获得1000点积分和其他场景。
以注册新用户的用例为例,为什么需要异步处理?主要有两个方面:
- 容错性和鲁棒性:如果发送邮件时发生异常,用户注册不会因为邮件发送失败而失败。因为用户注册是主要功能,发送电子邮件是次要功能。当用户无法接收电子邮件但可以查看并登录系统时,他们不会关心电子邮件。
- 提高界面性能:例如,注册用户需要20毫秒,发送电子邮件需要2000毫秒。如果使用同步模式,总时间消耗约为2020毫秒,用户可以明显感觉到迟缓。但如果使用异步模式,注册可以在短短几十毫秒内成功。这是一个即时的事情。
SpringBoot如何实现异步调用?
在了解了为什么需要异步调用之后,让我们看看SpringBoot如何实现这样的代码。
其实很简单。从Spring 3开始,提供了@Async
annotation。我们只需要在方法上标记这个注释,这个方法就可以实现异步调用。
当然,我们还需要一个配置类,并将注释@EnableAsync
添加到配置类中以启用异步函数。
第一步:创建配置类并启用异步函数支持
使用@EnableAsync
启用异步任务支持。@EnableAsync
annotation可以直接放在Spring Boot
startup类上,也可以单独放在其他配置类上。这里我们选择使用一个单独的配置类SyncConfiguration
。
@Configuration
@EnableAsync
public class AsyncConfiguration {
// do nothing
}
第二步:将方法标记为异步调用
为业务处理添加一个Component
类。同时,添加@Async
注释,表示此方法是异步处理。
@Component
public class AsyncTask {
@Async
public void sendEmail() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
System.out.println("Sending an email took " + (t2-t1) + " ms");
}
}
第三步:在Controller中调用异步方法。
@RestController
@RequestMapping("/user")
public class AsyncController {
@Autowired
private AsyncTask asyncTask;
@RequestMapping("/register")
public void register() throws InterruptedException {
long t1 = System.currentTimeMillis();
// Simulate the time required for user registration.
Thread.sleep(20);
// Registration is successful. Send an email.
asyncTask.sendEmail();
long t2 = System.currentTimeMillis();
System.out.println("Registering a user took " + (t2-t1) + " ms");
}
}
访问http://localhost:8080/user/register
后,查看控制台日志。
Registering a user took 29 ms
Sending an email took 2006 ms
从日志中可以看出,主线程不需要等待邮件发送方法的执行完成后才返回,有效减少了响应时间,提高了界面性能。
通过以上三个步骤,我们可以轻松地使用Spring Boot中的异步方法来提高我们的接口性能。不是很简单吗?
但是,如果你真的在公司项目中这样实现它,在代码审查时肯定会被拒绝,甚至可能会被训斥。😫
因为上面的代码忽略了最大的问题,那就是没有为@Async
异步框架提供自定义线程池。
为什么要为@Async定制线程池?
在使用@Async
注释时,默认情况下使用SimpleAsyncTaskExecutor
线程池。此线程池不是真正的线程池。
使用此线程池无法实现线程重用。每次调用时都会创建一个新线程。如果在系统中不断创建线程,最终将导致系统过度使用内存,并导致OutOfMemoryError
!
关键代码如下:
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
Runnable taskToUse = this.taskDecorator != null ? this.taskDecorator.decorate(task) : task;
// Determine whether rate limiting is enabled. By default, it is not enabled.
if (this.isThrottleActive() && startTimeout > 0L) {
// Perform pre-operations and implement rate limiting.
this.concurrencyThrottle.beforeAccess();
this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
} else {
// In the case of no rate limiting, execute thread tasks.
this.doExecute(taskToUse);
}
}
protected void doExecute(Runnable task) {
// Continuously create threads.
Thread thread = this.threadFactory != null ? this.threadFactory.newThread(task) : this.createThread(task);
thread.start();
}
public Thread createThread(Runnable runnable) {
//Specify the thread name, task-1, task-2, task-3...
Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());
thread.setPriority(this.getThreadPriority());
thread.setDaemon(this.isDaemon());
return thread;
}
如果再次输出线程名,很容易发现,每次打印的线程名都是以[task-1]、[task-2]、[task-3]、[task-4]的形式,后面的序号是不断增加的。
因此,当在Spring中使用@Async
异步框架时,我们必须自定义一个线程池来替换默认的SimpleAsyncTaskExecutor
。
Spring提供了多种线程池可供选择:
SimpleAsyncTaskExecutor
:不是一个真实的线程池。此类不重用线程,每次调用时都会创建一个新线程。SyncTaskExecutor
:这个类不实现异步调用,只是一个同步操作。仅适用于不需要多线程的场景。ConcurrentTaskExecutor
:Executor的适配类。不推荐.仅当ThreadPoolTaskExecutor不满足要求时才考虑使用此类。ThreadPoolTaskScheduler
:可以使用cron表达式。ThreadPoolTaskExecutor
:最常用和推荐的。本质上,它是java.util.concurrent.ThreadPoolExecutor的包装器。
SimpleAsyncTaskExecutor
和ThreadPoolTaskExecutor
的实现路径如下:
实现自定义线程池
让我们直接看看代码实现。这里,实现了一个名为asyncPoolTaskExecutor
的线程池:
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// Core thread count.
taskExecutor.setCorePoolSize(10);
// The maximum number of threads maintained in the thread pool. Only when the buffer queue is full will threads exceeding the core thread count be requested.
taskExecutor.setMaxPoolSize(100);
// Cache queue.
taskExecutor.setQueueCapacity(50);
// Allowed idle time. Threads other than core threads will be destroyed after the idle time arrives.
taskExecutor.setKeepAliveSeconds(200);
// Thread name prefix for asynchronous methods.
taskExecutor.setThreadNamePrefix("async-");
/**
* When the task cache queue of the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, if there are still tasks coming, a task rejection policy will be adopted.
* There are usually four policies:
* ThreadPoolExecutor.AbortPolicy: Discard the task and throw RejectedExecutionException.
* ThreadPoolExecutor.DiscardPolicy: Also discard the task, but do not throw an exception.
* ThreadPoolExecutor.DiscardOldestPolicy: Discard the task at the front of the queue and then try to execute the task again (repeat this process).
* ThreadPoolExecutor.CallerRunsPolicy: Retry adding the current task and automatically call the execute() method repeatedly until it succeeds.
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
恭喜你!定制线程池后,我们可以大胆地使用@Async
提供的异步处理能力。😊
配置多个线程池
在真实的Internet项目的开发中,对于高并发请求,一般的方法是用单独的线程池隔离高并发接口进行处理。
假设当前有两个高并发接口。通常,根据接口特性定义两个线程池。此时,当我们使用@Async
时,我们需要通过指定不同的线程池名称进行区分。
为@Async指定特定的线程池
@Async("myAsyncPoolTaskExecutor")
public void sendEmail() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
System.out.println("Sending an email took " + (t2-t1) + " ms");
}
当系统中有多个线程池时,我们也可以配置一个默认线程池。对于非默认异步任务,我们可以通过@Async("otherTaskExecutor")
指定线程池名称。
配置默认线程池
可以修改配置类来实现AsyncConfigurer
并覆盖getAsyncExecutor()
方法来指定默认线程池:
@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {
@Bean(name = "myAsyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
// Initialization code for thread pool configuration as above.
}
@Bean(name = "otherTaskExecutor")
public ThreadPoolTaskExecutor otherExecutor() {
// Initialization code for thread pool configuration as above.
}
/**
* Specify the default thread pool.
*/
@Override
public Executor getAsyncExecutor() {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("An unknown error occurred while executing tasks in the thread pool. Executing method: {}", method.getName(), ex);
}
}
如下所示,sendEmail()
方法使用默认的线程池myAsyncPoolTaskExecutor
,而otherTask()
方法使用线程池otherTaskExecutor
,这是非常灵活的。
@Async("myAsyncPoolTaskExecutor")
public void sendEmail() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
System.out.println("Sending an email took " + (t2-t1) + " ms");
}
@Async("otherTaskExecutor")
public void otherTask() {
//...
}