博文地址:https://sourl.cn/URptixjava
当一个 HTTP 请求到达 Tomcat,Tomcat 将会从线程池中取出线程,而后按照以下流程处理请求:web
HttpServletRequest
HttpServletResponse
将响应结果返回给等待客户端总体流程以下所示:spring
这是咱们平常最经常使用同步请求模型,全部动做都交给同一个 Tomcat 线程处理,全部动做处理完成,线程才会被释放回线程池。浏览器
想象一下若是业务须要较长时间处理,那么这个 Tomcat 线程其实一直在被占用,随着请求愈来愈多,可用 I/O 线程愈来愈少,直到被耗尽。这时后续请求只能等待空闲 Tomcat 线程,这将会加长了请求执行时间。spring-mvc
若是客户端不关心返回业务结果,这时咱们能够自定义线程池,将请求任务提交给线程池,而后马上返回。多线程
也可使用 Spring Async 任务,你们感兴趣能够自行查找一下资料并发
可是不少场景下,客户端须要处理返回结果,咱们没办法使用上面的方案。在 Servlet2 时代,咱们没办法优化上面的方案。mvc
不过等到 Servlet3 ,引入异步 Servelt 新特性,能够完美解决上面的需求。app
异步 Servelt 执行请求流程:异步
HttpServletRequest
Servlet
处理,将业务提交给自定义业务线程池,请求马上返回,Tomcat 线程马上被释放HttpServletResponse
将响应结果返回给等待客户端引入异步 Servelt3 总体流程以下:
使用异步 Servelt,Tomcat 线程仅仅处理请求解析动做,全部耗时较长的业务操做所有交给业务线程池,因此相比同步请求, Tomcat 线程能够处理 更对请求。
虽然咱们将业务处理交给业务线程池异步处理,可是对于客户端来说,其还在同步等待响应结果。
可能有些同窗会以为异步请求将会得到更快响应时间,其实不是的,相反可能因为引入了更多线程,增长线程上下文切换时间。
虽然没有下降响应时间,可是经过请求异步化带来其余明显优势:
因此具体使用过程,咱们还须要进行的相应的压测,观察响应时间以及吞吐量等其余指标,综合选择。
异步 Servelt 使用方式不是很难,小黑哥总结就是就是下面三板斧:
HttpServletRequest#startAsync
获取 AsyncContext
异步上下文对象AsyncContext#complete
返回响应结果下面的例子将会使用 SpringBoot ,Web 容器选择 Tomcat
示例代码以下:
ExecutorService executorService = Executors.newFixedThreadPool(10); @RequestMapping("/hello") public void hello(HttpServletRequest request) { AsyncContext asyncContext = request.startAsync(); // 超时时间 asyncContext.setTimeout(10000); executorService.submit(() -> { try { // 休眠 5s,模拟业务操做 TimeUnit.SECONDS.sleep(5); // 输出响应结果 asyncContext.getResponse().getWriter().println("hello world"); log.info("异步线程处理结束"); } catch (Exception e) { e.printStackTrace(); } finally { asyncContext.complete(); } }); log.info("servlet 线程处理结束"); }
浏览器访问该请求将会同步等待 5s 获得输出响应,应用日志输出结果以下:
2020-03-24 07:27:08.997 INFO 79257 --- [nio-8087-exec-4] com.xxxx : servlet 线程处理结束 2020-03-24 07:27:13.998 INFO 79257 --- [pool-1-thread-3] com.xxxx : 异步线程处理结束
这里咱们须要注意设置合理的超时时间,防止客户端长时间等待。
Servlet3 API ,没法使用 SpringMVC 为咱们提供的特性,咱们须要本身处理响应信息,处理方式相对繁琐。
SpringMVC 3.2 基于 Servelt3 引入异步请求处理方式,咱们能够跟使用同步请求同样,方便使用异步请求。
SpringMVC 提供有两种异步方式,只要将 Controller
方法返回值修改下述类便可:
DeferredResult
Callable
DeferredResult
是 SpringMVC 3.2 以后引入新的类,只要让请求方法返回 DeferredResult
,就能够快速使用异步请求,示例代码以下:
ExecutorService executorService = Executors.newFixedThreadPool(10); @RequestMapping("/hello_v1") public DeferredResult<String> hello_v1() { // 设置超时时间 DeferredResult<String> deferredResult = new DeferredResult<>(7000L); // 异步线程处理结束,将会执行该回调方法 deferredResult.onCompletion(() -> { log.info("异步线程处理结束"); }); // 若是异步线程执行时间超过设置超时时间,将会执行该回调方法 deferredResult.onTimeout(() -> { log.info("异步线程超时"); // 设置返回结果 deferredResult.setErrorResult("timeout error"); }); deferredResult.onError(throwable -> { log.error("异常", throwable); // 设置返回结果 deferredResult.setErrorResult("other error"); }); executorService.submit(() -> { try { TimeUnit.SECONDS.sleep(5); deferredResult.setResult("hello_v1"); // 设置返回结果 } catch (Exception e) { e.printStackTrace(); // 若异步方法内部异常 deferredResult.setErrorResult("error"); } }); log.info("servlet 线程处理结束"); return deferredResult; }
建立 DeferredResult
实例时能够传入特定超时时间。另外咱们能够设置默认超时时间:
# 异步请求超时时间 spring.mvc.async.request-timeout=2000
若是异步程序执行完成,能够调用 DeferredResult#setResult
返回响应结果。此时如有设置 DeferredResult#onCompletion
回调方法,将会触发该回调方法。
最后 DeferredResult
还提供其余异常的回调方法 onError
,起初小黑哥觉得只要异步线程内发生异常,就会触发该回调方法。尝试在异步线程内抛出异常,可是没法成功触发。
后续小黑哥查看这个方法的 doc,当 web 容器线程处理异步请求是时发生异常,才能成功触发。
小黑哥不知道如何才能发生这个异常,有经验的小伙伴们的能够留言告知下。
Spring 另外还提供一种异步请求使用方式,直接使用 JDK Callable
。示例代码以下:
@RequestMapping("/hello_v2") public Callable<String> hello_v2() { return new Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(5); log.info("异步方法结束"); return "hello_v2"; } }; }
默认状况下,直接执行将会输出 WARN 日志:
这是由于默认状况使用 SimpleAsyncTaskExecutor
执行异步请求,每次调用执行都将会新建线程。因为这种方式不复用线程,生产不推荐使用这种方式,因此咱们须要使用线程池代替。
咱们可使用以下方式自定义线程池:
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) public AsyncTaskExecutor executor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setThreadNamePrefix("test-"); threadPoolTaskExecutor.setCorePoolSize(10); threadPoolTaskExecutor.setMaxPoolSize(20); return threadPoolTaskExecutor; }
注意 Bean 名称必定要是 applicationTaskExecutor
,若不一致, Spring 将不会使用自定义线程池。
或者能够直接使用 SpringBoot 配置文件方式配置代替:
# 核心线程数 spring.task.execution.pool.core-size=10 # 最大线程数 spring.task.execution.pool.max-size=20 # 线程名前缀 spring.task.execution.thread-name-prefix=test # 还有另一些配置,读者们能够自行配置
这种方式异步请求的超时时间只能经过配置文件方式配置。
spring.mvc.async.request-timeout=10000
若是须要为单独请求的配置特定的超时时间,咱们须要使用 WebAsyncTask
包装 Callable
。
@RequestMapping("/hello_v3") public WebAsyncTask<String> hello_v3() { System.out.println("asdas"); Callable<String> callable=new Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(5); log.info("异步方法结束"); return "hello_v3"; } }; // 单位 ms WebAsyncTask<String> webAsyncTask=new WebAsyncTask<>(10000,callable); return webAsyncTask; }
SpringMVC 两种异步请求方式,本质上就是帮咱们包装 Servlet3 API ,让咱们不用关心具体实现细节。虽然平常使用咱们通常会选择使用 SpringMVC 两种异步请求方式,可是咱们仍是须要了解异步请求实际原理。因此你们若是在使用以前,能够先尝试使用 Servlet3 API 练习,后续再使用 SpringMVC。
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:studyidea.cn