背景:做项目过程中,一些耗时长的任务可能需要在后台线程池中运行;典型的如发送邮件等,由于需要调用外部的接口来进行实际的发送操作,如果客户端在提交发送请求后一直等待服务器端发送成功后再返回,就会长时间的占用服务器的一个连接;当这类请求过多时,服务器连接数会不够用,新的连接请求可能无法得到满足,从而导致客户端连接失败。因此如果 request(/url) 经过dispatcherServlet 找到对应的 controller中请求方法后,先去释放request 线程资源,通过异步调用的方式去处理contorller方法 中接下来要执行代码,当异步线程 执行完后,controller 方法返回处理的值,这样就不会因为 大量请求,服务器没法处理连接问题。
后端Java层 异步调用,实现 方式就是 采用多创建一个线程的方式去实现。
当然,创建一个线程,对jvm的性能影响不大,但如果每个请求都去创建一个实现异步的线程,这种开销解决请求堵塞问题有种太空间换时间(或者说请求响应度)的了,因此推荐使用线程池的方式去是实现TaskExecuter。
异步请求实现流程
无论是使用注解Callable 或 WebAsyncTask的方式 流程都是为:当 dispatcherServlet 去找到了对应的请求方法时,请求线程 结束该线程,让出线程资源,将响应保持打开状态,异步线程去执行代码,springmvc重新分配一个request请求,该线程去将异步执行的结果返回,然后返回视图。
方式一:从相比之前,控制器方法不一定需要返回一个值,而是 可以返回一个Callable<> 的一个对象
/**
* 异步调用restful
* 当controller返回值是Callable的时候,springmvc就会启动一个线程将Callable交给TaskExecutor去处理
* 然后DispatcherServlet还有所有的spring拦截器都退出主线程,然后把response保持打开的状态
* 当Callable执行结束之后,springmvc就会重新启动分配一个request请求,然后DispatcherServlet就重新
* 调用和处理Callable异步执行的返回结果, 然后返回视图
*
* @return
*/
@GetMapping("/hello")
public Callable<String> helloController() {
logger.info(Thread.currentThread().getName() + " 进入helloController方法");
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " 进入call方法");
String say = hello.sayHello();
logger.info(Thread.currentThread().getName() + " 从helloService方法返回");
return say;
}
};
logger.info(Thread.currentThread().getName() + " 从helloController方法返回");
return callable;
}
容器的线程http-nio-8060-exec-1这个线程进入controller之后,就立即返回了,具体的服务调用是通过MvcAsync2这个线程来做的,当服务执行完要返回后,容器会再启一个新的线程http-nio-8060-exec-2来将结果返回给客户端或浏览器,整个过程response都是打开的,当有返回的时候,再从server端推到response中去。
2020-07-20 18:51:48.366 INFO 6068 --- [nio-8011-exec-1] c.c.j.controller.HelloController : http-nio-8011-exec-1 进入helloController方法 2020-07-20 18:51:48.367 INFO 6068 --- [nio-8011-exec-1] c.c.j.controller.HelloController : http-nio-8011-exec-1 从helloController方法返回 2020-07-20 18:51:48.374 INFO 6068 --- [ task-1] c.c.j.controller.HelloController : task-1 进入call方法 2020-07-20 18:51:48.375 INFO 6068 --- [ task-1] c.c.j.controller.HelloController : task-1 从helloService方法返回
@RequestMapping("/deletePerson1")
@ResponseBody
public WebAsyncTask<Boolean> deletePerson2(@RequestParam(name = "pid",value = "pid") final int pid) {
//返回值为什么类型 callable 中 就什么类型
Callable<Boolean> callable= new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return personService.deletePesson(pid);
}
}; //设置超时时间, 为10s
WebAsyncTask<Boolean> webAsyncTask= new WebAsyncTask<Boolean>(10*1000L,callable);
log.info("异步测试 使用 webAsyncTask 删除");
return webAsyncTask;
}
方式三: DeferredResult可以处理一些相对复杂一些的业务逻辑,最主要还是可以在另一个线程里面进行业务处理及返回,即可在两个完全不相干的线程间的通信。
说明:
你也可以配置用于执行控制器返回值Callable
的执行器AsyncTaskExecutor
。Spring强烈推荐你配置这个选项,因为Spring MVC默认使用的是普通的执行器SimpleAsyncTaskExecutor
,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。所以最好我们来自定义一个线程池。MVC Java编程配置及MVC命名空间配置的方式都允许你注册自己的CallableProcessingInterceptor
和DeferredResultProcessingInterceptor
拦截器实例。
如果一个业务逻辑执行完成需要多个步骤,也就是调用多个方法去执行,这个时候异步执行比同步执行相应更快。
以下是官方已经实现的全部7个TaskExecuter。Spring宣称对于任何场景,这些TaskExecuter完全够用了:
不使用 异步的情况下,正常处理请求的线程为[http-nio-8080-exec-2] springmvc 线程
去post 提交一个 {"id":1,"name":"我的世界"} json 数据
[2020-07-20 15:26:36,387] [INFO ] [http-nio-8080-exec-2] [Initializing Spring DispatcherServlet 'dispatcherServlet'] [2020-07-20 15:26:36,387] [INFO ] [http-nio-8080-exec-2] [Initializing Servlet 'dispatcherServlet'] [2020-07-20 15:26:36,397] [INFO ] [http-nio-8080-exec-2] [Completed initialization in 10 ms] [2020-07-20 15:26:36,517] [INFO ] [http-nio-8080-exec-2] [] - [] [] [] [/asyncD] - [DEFAULT] [0] [Servlet thread released] [AsyncController.executeSlowTaskB(104)] [] [] [AsyncController.executeSlowTaskB(98)] [controller strat:27] [] [2020-07-20 15:26:36,948] [INFO ] [http-nio-8080-exec-2] [] - [] [] [] [/asyncD] - [PROCESS] [0] [27] [AsyncService.execute(34)] [] [] [AsyncController.executeSlowTaskB(98);AsyncController.executeSlowTaskB(105);AsyncService.execute(21);AsyncService.execute(29)] [controller strat:27;controller end:27;thread start:27;count :49995008] [1] [2020-07-20 15:26:36,949] [INFO ] [http-nio-8080-exec-2] [430] - [] [] [] [/asyncD] - [INVOKE] [0] [异步sevice] [AsyncService.execute(..)] [[{"id":1,"name":"我的世界"}]] [{"id":1,"name":"我的世界","ret":"27"}] [AsyncController.executeSlowTaskB(98);AsyncController.executeSlowTaskB(105);AsyncService.execute(21);AsyncService.execute(29)] [controller strat:27;controller end:27;thread start:27;count :49995008] [2]
首先 第一步 在springboot 启动类上加上注解支持异步调用的方式 @EnableAsync注解。
然后在在要实现异步操作的service 层中的方法加上 @Async 注解。
请求与上面 一致的 url 查看控制台 打印的执行线程为 task-1
Spring MVC默认使用的是普通的执行器SimpleAsyncTaskExecutor
。
[2020-07-20 15:15:33,398] [INFO ] [task-1] [] - [] [] [] [] - [PROCESS] [0] [36] [AsyncService.execute(34)] [] [] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:36;count :49995008] [1] [2020-07-20 15:15:33,399] [INFO ] [task-1] [415] - [] [] [] [] - [INVOKE] [0] [异步sevice] [AsyncService.execute(..)] [[{"id":1,"name":"我的世界"}]] [{"id":1,"name":"我的世界","ret":"36"}] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:36;count :49995008] [2]
配置 线程池实现的方式,需要加上配置类如下
@Configuration
@ComponentScan("com.xxx.common.base.sample.log")
public class SyncConfig {
@Bean
public Executor getExecutor() {
//初始化任务执行线程池
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
executor.setThreadNamePrefix("线程池中的线程");
return executor;
}
}
打印出的 控制台信息如下
2020-07-20 15:35:56,746] [INFO ] [http-nio-8080-exec-1] [] - [] [] [] [/asyncD] - [DEFAULT] [0] [Servlet thread released] [AsyncController.executeSlowTaskB(104)] [] [] [AsyncController.executeSlowTaskB(98)] [controller strat:26] []
[2020-07-20 15:35:57,183] [INFO ] [线程池中的线程1] [] - [] [] [] [] - [PROCESS] [0] [37] [AsyncService.execute(34)] [] [] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:37;count :49995008] [1]
[2020-07-20 15:35:57,184] [INFO ] [线程池中的线程1] [433] - [] [] [] [] - [INVOKE] [0] [异步sevice] [AsyncService.execute(..)] [[{"id":1,"name":"我的世界"}]] [{"id":1,"name":"我的世界","ret":"37"}] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:37;count :49995008] [2]
再请求一次:
[2020-07-20 15:39:05,022] [INFO ] [http-nio-8080-exec-2] [] - [] [] [] [/asyncD] - [DEFAULT] [0] [Servlet thread released] [AsyncController.executeSlowTaskB(104)] [] [] [AsyncController.executeSlowTaskB(98)] [controller strat:27] [] [2020-07-20 15:39:05,467] [INFO ] [线程池中的线程2] [] - [] [] [] [] - [PROCESS] [0] [45] [AsyncService.execute(34)] [] [] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:45;count :49995008] [1] [2020-07-20 15:39:05,468] [INFO ] [线程池中的线程2] [445] - [] [] [] [] - [INVOKE] [0] [异步sevice] [AsyncService.execute(..)] [[{"id":1,"name":"我的世界"}]] [{"id":1,"name":"我的世界","ret":"45"}] [AsyncService.execute(21);AsyncService.execute(29)] [thread start:45;count :49995008] [2]
可以看到每次请求,第一次为spirngmvc的线程,第二三次 都是使用异步操作完成的线程,如果不使用异步操作那么一个请求完成 都会是三次 springmvc 线程去完成的,异步减轻了 dispatcherServlet处理多个请求 的负担。
使用Async注解 的两个约束
约束一 调用者和@Async 修饰的方法必须定义在两个类中,调用者比如为controller 中的方法,@Async去修饰service 中的方法。
约束二 @Async和@PostConstruct不能同时在同一个类中使用 ,@PostConstruct注解是会在spring 框架初始化bean 时起到的作用(详情:https://blog.csdn.net/qq360694660/article/details/82877222)
参考原文链接
https://www.jianshu.com/p/ecc6f5168aef
https://www.jianshu.com/p/ecc6f5168aef
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。