前言

当一个 HTTP 请求到达 Tomcat,Tomcat 将会从线程池中取出线程,然后按照如下流程处理请求:

  • 将请求信息解析为 HttpServletRequest
  • 分发到具体 Servlet 处理相应的业务
  • 通过 HttpServletResponse 将响应结果返回给等待客户端

整体流程如下所示:

这是我们日常最常用同步请求模型,所有动作都交给同一个 Tomcat 线程处理,所有动作处理完成,线程才会被释放回线程池。

想象一下如果业务需要较长时间处理,那么这个 Tomcat 线程其实一直在被占用,随着请求越来越多,可用 I/O 线程越来越少,直到被耗尽。这时后续请求只能等待空闲 Tomcat 线程,这将会加长了请求执行时间。

如果客户端不关心返回业务结果,这时我们可以自定义线程池,将请求任务提交给线程池,然后立刻返回。

也可以使用 Spring Async 任务,大家感兴趣可以自行查找一下资料

但是很多场景下,客户端需要处理返回结果,我们没办法使用上面的方案。在 Servlet2 时代,我们没办法优化上面的方案。

不过等到 Servlet3 ,引入异步 Servlet 新特性,可以完美解决上面的需求。

异步 Servlet 执行请求流程:

  • 将请求信息解析为 HttpServletRequest
  • 分发到具体 Servlet 处理,将业务提交给自定义业务线程池,请求立刻返回,Tomcat 线程立刻被释放
  • 当业务线程将任务执行结束,将会将结果转交给 Tomcat 线程
  • 通过 HttpServletResponse 将响应结果返回给等待客户端

引入异步 Servlet3 整体流程如下:

使用异步 Servelt,Tomcat 线程仅仅处理请求解析动作,所有耗时较长的业务操作全部交给业务线程池,所以相比同步请求, Tomcat 线程可以处理 更多请求。

虽然我们将业务处理交给业务线程池异步处理,但是对于客户端来讲,其还在同步等待响应结果

可能有些同学会觉得异步请求将会获得更快响应时间,其实不是的,相反可能由于引入了更多线程,增加线程上下文切换时间。

虽然没有降低响应时间,但是通过请求异步化带来其他明显优点

  • 可以处理更高并发连接数,提高系统整体吞吐量
  • 请求解析与业务处理完全分离,职责单一
  • 自定义业务线程池,我们可以更容易对其监控,降级等处理
  • 可以根据不同业务,自定义不同线程池,相互隔离,不用互相影响

所以具体使用过程,我们还需要进行的相应的压测,观察响应时间以及吞吐量等其他指标,综合选择。

异步 Servelt 使用方式

异步 Servelt 使用方式不是很难,阿粉总结就是下面三板斧:

  1. HttpServletRequest#startAsync 获取 AsyncContext 异步上下文对象
  2. 使用自定义的业务线程池处理业务逻辑
  3. 业务线程处理结束,通过 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 hello_v1() {    // 设置超时时间    DeferredResult 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#onTimeout,一旦异步线程执行超时,将会触发该回调方法。

最后 DeferredResult 还提供其他异常的回调方法 onError,起初阿粉以为只要异步线程内发生异常,就会触发该回调方法。尝试在异步线程内抛出异常,但是无法成功触发。

后续阿粉查看这个方法的 doc,当 web 容器线程处理异步请求时发生异常,才能成功触发。

Callable

Spring 另外还提供一种异步请求使用方式,直接使用 JDK Callable。示例代码如下:

@RequestMapping("/hello_v2")public Callable hello_v2() {    return new Callable() {        @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 hello_v3() {    System.out.println("asdas");    Callable callable=new Callable() {        @Override        public String call() throws Exception {            TimeUnit.SECONDS.sleep(5);            log.info("异步方法结束");            return "hello_v3";        }    };    // 单位 ms    WebAsyncTask webAsyncTask=new WebAsyncTask<>(10000,callable);    return webAsyncTask;}

总结

SpringMVC 两种异步请求方式,本质上就是帮我们包装 Servlet3 API ,让我们不用关心具体实现细节。虽然日常使用我们一般会选择使用 SpringMVC 两种异步请求方式,但是我们还是需要了解异步请求实际原理。所以大家如果在使用之前,可以先尝试使用 Servlet3 API 练习,后续再使用 SpringMVC。

httpservletrequest 设置请求头_大部分程序员不知道的 Servelt3 异步请求,原来这么简单?相关推荐

  1. 有哪些新手程序员不知道的小技巧?

    提到新手程序员,大家想到的第一个词可能就是:刷题.尤其是通过LeetCode刷题,想必新手程序员们都经历过这一步,甚至不少人认为只要在LeetCode上刷的题目够多,就一定能够进阶为大神. 但是,不难 ...

  2. 99%程序员不知道的编程必备工具,人工智能助你编程更轻松

    最近在浏览技术社区,发现了一款能节省开发时间的 IDE 插件.是一个利用人工智能技术帮助程序员提高编程效率,节省时间的工具,它的功能简直是为了我们程序员量身定做的,一下勾起了我的好奇心. 这款插件叫a ...

  3. 有哪些初学者程序员不知道的小技巧?

    提到新手程序员,大家想到的第一个词可能就是--刷题.尤其是通过LeetCode刷题,想必新手程序员们都经历过这一步,甚至不少人认为只要在LeetCode上刷的题目够多,就一定能够进阶为大神. 但是,不 ...

  4. 前端新手程序员不知道的 20个小技巧

    1.作为前端开发者,使用双显示器能大幅提高开发效率. 2.学编程最好的语言不是PHP,是English. 3.东西交付之前偷偷测试一遍. 4.问别人之前最好先自己百度,google一下,以免问出太低级 ...

  5. 新手程序员不知道的小技巧!

    1.作为前端开发者,使用双显示器能大幅提高开发效率. 2.学编程最好的语言不是PHP,是English. 3.东西交付之前偷偷测试一遍. 4.问别人之前最好先自己百度,google一下,以免问出太低级 ...

  6. 师妹问我:有哪些新手程序员不知道的小技巧?

    阅读本文大概需要5分钟. 一个师妹问:洋哥,我今年应届毕业,刚开始写代码,不知道有没有一些新手需要注意的地方. 给了师妹一些建议之后,感觉这是个好问题!不光是新手程序员,很多小技巧小秘密恐怕老手也未必 ...

  7. ajax jq 图片上传请求头_如何使用js或jQuery向Ajax请求添加自定义HTTP头?

    下面是一个使用XHR 2的示例:function xhrToSend(){ // Attempt to creat the XHR2 object var xhr; try{ xhr = new XM ...

  8. 80%程序员不知道的职场秘诀,升职加薪不是梦

    作者 | 梁唐 来源 | TechFlow(ID:techflow2019) 头图 |  CSDN 下载自东方IC 在互联网行业,我们经常用一个标准去评价一个人,这个标准就是ownership.一个有 ...

  9. as3程序员不知道的as2和as3的不同点

    最近,因为项目开发需要,得用as2实现一些较复杂的功能,发现as2的和as3的一些差异,总结如下: 1.as2没有as3中const关键字 2.as2没有as3中包的概念,包路径在类名处定义.类没有p ...

最新文章

  1. 24GHz和77GHz毫米波雷达技术细节
  2. 模型优化的风向标:偏差与方差
  3. 水平拉滑轮组计算机械效率的题,机械效率杠杆论文,关于中考物理机械效率计算题*相关参考文献资料-免费论文范文...
  4. 7、Power Query-合并查询
  5. 在线小词典(mysql扩展库操作)
  6. BBSSDK论坛移动化实现方案
  7. Win10 JAVA安装及环境搭建(windows jdk,windows java环境配置)
  8. 解决 service、killall 等命令找不到的问题
  9. 使用celery执行Django串行异步任务
  10. 2021“数维杯”国际大学生数学建模竞赛A题思路
  11. 锐起2540无盘教程
  12. 彻底卸载流氓软件如360等 1. 使用卸载软件(附下载链接) 2. 安全模式删除两种方法
  13. HBuilderX搭建Vue项目
  14. 【业界新闻】浪潮高端存储系统技术发展及展望(上篇)
  15. 实现回到顶部功能的三种方法
  16. git rebase
  17. 车载网络: 常见车载网络
  18. B2B、B2C、C2C、O2O分别是什么
  19. 【Redis Cluster集群】redis cluster 多mster写入,读写分离,高可用
  20. 分享10大自动化测试框架,你用过几个?

热门文章

  1. n对括号问题,(求n对括号的正确排列有多少)
  2. 如何在Evolution中加密(一)
  3. Postman下一个接口要用到上一个接口的数据
  4. SQL Server 日期和时间函数
  5. HTML5 —— 属性
  6. unity3D与网页的交互---做项目的一点总结
  7. C++ STL算法系列4---unique , unique_copy函数
  8. 20120510,OSPF第三部分
  9. 装ie9后无法用网银解决办法
  10. 每天学一点flash(76)百度MP3音乐APi接口使用