Spring中的异步请求、异步调用及demo测试

时间:2022-07-22
本文章向大家介绍Spring中的异步请求、异步调用及demo测试,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

背景:做项目过程中,一些耗时长的任务可能需要在后台线程池中运行;典型的如发送邮件等,由于需要调用外部的接口来进行实际的发送操作,如果客户端在提交发送请求后一直等待服务器端发送成功后再返回,就会长时间的占用服务器的一个连接;当这类请求过多时,服务器连接数会不够用,新的连接请求可能无法得到满足,从而导致客户端连接失败。因此如果 request(/url) 经过dispatcherServlet 找到对应的 controller中请求方法后,先去释放request 线程资源,通过异步调用的方式去处理contorller方法 中接下来要执行代码,当异步线程 执行完后,controller 方法返回处理的值,这样就不会因为 大量请求,服务器没法处理连接问题。

  后端Java层 异步调用,实现 方式就是 采用多创建一个线程的方式去实现。

当然,创建一个线程,对jvm的性能影响不大,但如果每个请求都去创建一个实现异步的线程,这种开销解决请求堵塞问题有种太空间换时间(或者说请求响应度)的了,因此推荐使用线程池的方式去是实现TaskExecuter。

Springboot 中的处理异步请求

异步请求实现流程

  无论是使用注解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命名空间配置的方式都允许你注册自己的CallableProcessingInterceptorDeferredResultProcessingInterceptor拦截器实例。

异步请求与异步调用的区别

  • 两者的使用场景不同,异步请求用来解决并发请求对服务器造成的压力,从而提高对请求的吞吐量;而异步调用是用来做一些非主线流程且不需要实时计算和响应的任务,比如同步日志到kafka中做日志分析等。
  • 异步请求是会一直等待response相应的,需要返回结果给客户端的;而异步调用我们往往会马上返回给客户端响应,完成这次整个的请求,至于异步调用的任务后台自己慢慢跑就行,客户端不会关心。

Springboot中实现 异步调用

如果一个业务逻辑执行完成需要多个步骤,也就是调用多个方法去执行,这个时候异步执行比同步执行相应更快。

以下是官方已经实现的全部7个TaskExecuter。Spring宣称对于任何场景,这些TaskExecuter完全够用了: 

  • ThreadPoolTaskExecutor (已测试) 它是最经常使用的一个,提供了一些Bean属性用于配置java.util.concurrent.ThreadPoolExecutor并且将其包装到TaskExecutor对象中。如果需要适配java.util.concurrent.Executor,请使用ConcurrentTaskExecutor。
  • SimpleAsyncTaskExecutor(已测试) 线程不会重用,每次调用时都会重新启动一个新的线程;但它有一个最大同时执行的线程数的限制;
  • SyncTaskExecutor 同步的执行任务,任务的执行是在主线程中,不会启动新的线程来执行提交的任务。主要使用在没有必要使用多线程的情况,如较为简单的测试用例。
  • ConcurrentTaskExecutor 它用于适配java.util.concurrent.Executor, 一般情况下请使用ThreadPoolTaskExecutor,如果hreadPoolTaskExecutor不够灵活时可以考虑采用ConcurrentTaskExecutor。
  • SimpleThreadPoolTaskExecutor 它是Quartz中SimpleThreadPool的一个实现,用于监听Spring生命周期回调事件。它主要使用在需要一个线程池来被Quartz和非Quartz中的对象同时共享使用的情况。
  • WorkManagerTaskExecutor 它实现了CommonJ中的WorkManager接口,是在Spring中使用CommonJ的WorkManager时的核心类。

不使用 异步的情况下,正常处理请求的线程为[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]

SimpleAsyncTaskExecutor 方式 (单线程,每次请求创建一个新的线程)

首先 第一步 在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]

ThreadPoolTaskExecutor方式(线程池 ) 最常用的方式

配置 线程池实现的方式,需要加上配置类如下

@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://blog.csdn.net/icarusliu/java/article/details/79528810

https://www.jianshu.com/p/ecc6f5168aef

https://www.jianshu.com/p/ecc6f5168aef

https://blog.csdn.net/qq360694660/article/details/82877222

https://www.cnblogs.com/baixianlong/p/10661591.html