前言
当一个 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 使用方式不是很难,阿粉总结就是下面三板斧:
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 : 异步线程处理结束
这里我们需要注意设置合理的超时时间,防止客户端长时间等待。