大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?
前言
当一个 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://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn
大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?的更多相关文章
- 程序员不得不知道的 API 接口常识
说实话,我非常希望两年前刚准备找实习的自己能看到本篇文章,那个时候懵懵懂懂,跟着网上的免费教程做了一个购物商城就屁颠屁颠往简历上写. 至今我仍清晰地记得,那个电商教程是怎么定义接口的: 管它是增加.修 ...
- 嵌入式程序员应知道的0x10个基本问题
来源:网络 嵌入式程序员应知道的0x10个基本问题 1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)#define SECONDS_PER_YEAR (60 ...
- PHP程序员应该知道的15个库
最几年,PHP已经成为最受欢迎的一种有效服务器端编程语言.据2013年发布的一份调查报告显示,PHP语言已经被安装在全球超过2.4亿个网站以及210万台Web服务器之上.PHP代表超文本预处理器,它主 ...
- Android 程序员必须知道的 53 个知识点
1. android 单实例运行方法 我们都知道 Android 平台没有任务管理器,而内部 App 维护者一个 Activity history stack 来实现窗口显示和销毁,对于常规从快捷方式 ...
- JS你可能还不知道的一些知识点(一)
js程序是用Unicode字符集编写的, 2.转义字符:反斜线 1 2 3 4 function Test(){ var s='you\'re right,it can\'t be a quote ...
- 嵌入式程序员应知道的0x10个C语言Tips
[1].[代码] [C/C++]代码 跳至 [1] ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ...
- [转载]或许您还不知道的八款Android开源游戏引擎
或许您还不知道的八款Android开源游戏引擎 分类: 技术文章 2010-08-04 20:27 17430人阅读 ...
- 成为嵌入式程序员应知道的0x10个基本问题
预处理器(Preprocessor)1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) #define SECONDS_PER_YEAR (60 * 60 * 2 ...
- Java程序员应该知道的10个面向对象理论
英文原文:10-object-oriented-design-principles 面向对象理论是面向对象编程的核心,但是我发现大部分 Java 程序员热衷于像单例模式.装饰者模式或观察者模式这样的设 ...
随机推荐
- 使用node打造自己的命令行
一.实现一个简单的功能 二.环境 1.系统: window 10 2.编辑器: vscode 3.node版本: 8.7.0 三.开始玩 1.打开命令行,新建一个pa'ckage.json npm i ...
- python数据分析工具 | pandas
pandas是python下强大的数据分析和探索工具,是的python在处理数据时非常快速.简单.它是构建在numpy之上的,包含丰富的数据处理函数,支持时间序列分析功能,支持灵活处理缺失数据. pa ...
- oracle根据特定字符拆分字符串的方法
清洗数据需要将某个字段内以空格分隔的字符串拆分成多行单个的字符串,百度了很多种方法大概归结起来也就这几种方法最为有效,现在把贴出来: 第一种: select regexp_substr('1 2 3' ...
- HashMap 速描
HashMap 速描 之前简单的整理了Java集合的相关知识,发现HashMap并不是三言两语能够讲明白的,所以专门整理一下HashMap的相关知识. HashMap 存储结构 HashMap是一个哈 ...
- oracle单机数据库搭建巨详细文档
规划 环境:redhat6.9 安装包:p13390677_112040_Linux-x86-64_1of7.zip p13390677_112040_Linux-x86-64_2of7.zip 数据 ...
- django 从零开始 制作一个图站 1环境的配置以及测试本地服务器
先使用用virtualenv建立一个虚拟环境 使用pycharm 建立一个django项目 选择虚拟环境和建立一个应用app 其中 tuzhan是项目根目录 user是我们的项目app 中间一些项目文 ...
- R自带数据集
向量 euro #欧元汇率,长度为11,每个元素都有命名landmasses #48个陆地的面积,每个都有命名precip #长度为70的命名向量rivers #北美141条河流长 ...
- rabitmq + php
消费者 <?php //配置信息 $conn_args = array( 'host' => '127.0.0.1', 'port' => '5672', 'login' => ...
- 题解 NOIP2018【赛道修建】—— 洛谷
这道题有一点点树上dp的意思(大佬轻喷 我刚拿到这道题的时候毫无头绪,只知道这道题要二分答案 为什么是二分答案??? 题目: 目前赛道修建的方案尚未确定.你的任务是设计一 种赛道修建的方案,使得修建的 ...
- 1.JVM中的五大内存区域划分详解及快速扫盲
本博客参考<深入理解Java虚拟机>这本书 视频及电子书详见:https://shimo.im/docs/HP6qqHx38xCJwcv9/ 一.快速扫盲 1. JVM是什么 JVM是 ...