前言
博文地址: https://sourl.cn/URptix
当一个 HTTP 请求到达 Tomcat,Tomcat 将会从线程池中取出线程, 然后按照如下流程处理请求:
将请求信息解析为 HttpServletRequest
分发到具体 Servlet 处理相应的业务
通过
HttpServletResponse
将响应结果返回给等待客户端
整体流程如下所示:
这是我们日常最常用同步请求模型, 所有动作都交给同一个 Tomcat 线程处理, 所有动作处理完成, 线程才会被释放回线程池.
想象一下如果业务需要较长时间处理, 那么这个 Tomcat 线程其实一直在被占用, 随着请求越来越多, 可用 I/O 线程越来越少, 直到被耗尽. 这时后续请求只能等待空闲 Tomcat 线程, 这将会加长了请求执行时间.
如果客户端不关心返回业务结果, 这时我们可以自定义线程池, 将请求任务提交给线程池, 然后立刻返回.
也可以使用 Spring Async 任务, 大家感兴趣可以自行查找一下资料
但是很多场景下, 客户端需要处理返回结果, 我们没办法使用上面的方案. 在 Servlet2 时代, 我们没办法优化上面的方案.
不过等到 Servlet3 , 引入异步 Servelt 新特性, 可以完美解决上面的需求.
异步 Servelt 执行请求流程:
将请求信息解析为 HttpServletRequest
分发到具体 Servlet 处理, 将业务提交给自定义业务线程池, 请求立刻返回, Tomcat 线程立刻被释放
当业务线程将任务执行结束, 将会将结果转交给 Tomcat 线程
通过
HttpServletResponse
将响应结果返回给等待客户端
引入异步 Servelt3 整体流程如下:
使用异步 Servelt,Tomcat 线程仅仅处理请求解析动作, 所有耗时较长的业务操作全部交给业务线程池, 所以相比同步请求, Tomcat 线程可以处理 更对请求.
虽然我们将业务处理交给业务线程池异步处理, 但是对于客户端来讲, 其还在同步等待响应结果.
可能有些同学会觉得异步请求将会获得更快响应时间, 其实不是的, 相反可能由于引入了更多线程, 增加线程上下文切换时间.
虽然没有降低响应时间, 但是通过请求异步化带来其他明显优点:
可以处理更高并发连接数, 提高系统整体吞吐量
请求解析与业务处理完全分离, 职责单一
自定义业务线程池, 我们可以更容易对其监控, 降级等处理
可以根据不同业务, 自定义不同线程池, 相互隔离, 不用互相影响
所以具体使用过程, 我们还需要进行的相应的压测, 观察响应时间以及吞吐量等其他指标, 综合选择.
异步 Servelt 使用方式
异步 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 : 异步线程处理结束
这里我们需要注意设置合理的超时时间, 防止客户端长时间等待.
SpringMVC
Servlet3 API , 无法使用 SpringMVC 为我们提供的特性, 我们需要自己处理响应信息, 处理方式相对繁琐.
SpringMVC 3.2 基于 Servelt3 引入异步请求处理方式, 我们可以跟使用同步请求一样, 方便使用异步请求.
SpringMVC 提供有两种异步方式, 只要将 Controller 方法返回值修改下述类即可:
- DeferredResult
- Callable
- DeferredResult
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 回调方法, 将会触发该回调方法.
Go to implementation(s)
最后 DeferredResult 还提供其他异常的回调方法 onError, 起初小黑哥以为只要异步线程内发生异常, 就会触发该回调方法. 尝试在异步线程内抛出异常, 但是无法成功触发.
后续小黑哥查看这个方法的 doc, 当 Web 容器线程处理异步请求是时发生异常, 才能成功触发.
小黑哥不知道如何才能发生这个异常, 有经验的小伙伴们的可以留言告知下.
Callable
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.
- Reference
- https://www.baeldung.com/spring-deferred-result
来源: https://www.cnblogs.com/goodAndyxublog/p/12641916.html