Tomcat是如何实现异步Servlet的 - 掘金
DeferredResult解决了什么问题 - 掘金
SpringMVC 异步处理模式分析
内容概要
- Tomcat 内部的异步请求实现
SpringMVC
的 DeferredResult
如何对接异步请求的
- Tomcat 过滤器在异步下的表现
Tomcat 内部的异步请求实现
我们直接借助 SpringBoot 框架来实现一个 Servlet,这里只展示 Servlet 代码:
上面的代码实现了一个异步的 Servlet,实现了 doGet
方法注意在 SpringBoot 中使用需要再启动类加上 @ServletComponentScan
注解来扫描 Servlet。既然代码写好了,我们来看看实际运行效果。
我们发送一个请求后,看到页面有响应,同时,看到请求时间花费了 10.05s,那么我们这个 Servlet 算是能正常运行啦。有同学肯定会问,这不是异步 servlet 吗?你的响应时间并没有加快,有什么用呢?对,我们的响应时间并不能加快,还是会取决于我们的业务逻辑,但是我们的异步 servlet 请求后,依赖于业务的异步执行,我们可以立即返回,也就是说,Tomcat 的线程可以立即回收,默认情况下,Tomcat 的核心线程是 10 ,最大线程数是 200 ,我们能及时回收线程,也就意味着我们能处理更多的请求,能够增加我们的吞吐量,这也是异步 Servlet 的主要作用。
异步 Servlet 的内部原理
了解完异步 Servlet 的作用后,我们来看看,Tomcat 是如何是先异步 Servlet 的。其实上面的代码,主要核心逻辑就两部分,final AsyncContext ctx = req.startAsync()
和 ctx.complete()
那我们来看看他们究竟做了什么?
我们发现 req.startAsync()
只是保存了一个异步上下文,同时设置一些基础信息,比如 Timeout
,顺便提一下,这里设置的默认超时时间是 30S ,如果你的异步处理逻辑超过 30S ,此时执行 ctx.complete()
就会抛出 IllegalStateException 异常。
我们来看看 ctx.complete()
的逻辑
所以,这里最终会调用 AbstractEndpoint
的 processSocket
方法,之前看过我前面博客的同学应该有印象,EndPoint
是用来接受和处理请求的,接下来就会交给 Processor
去进行协议处理。
这部分是重点,AbstractProcessorLight
会根据 SocketEvent
的状态来判断是不是要去调用 service(socketWrapper)
,该方法最终会去调用到容器,从而完成业务逻辑的调用,我们这个请求是执行完成后调用的,肯定不能进容器了,不然就是死循环了,这里通过 isAsync()
判断,就会进入 dispatch(status)
,最终会调用 CoyoteAdapter
的 asyncDispatch
方法
上面的代码就是 ctx.complete()
执行最终的方法了(当然省略了很多细节),完成了数据的输出,最终输出到浏览器。
这里有同学可能会说,我知道异步执行完后,调用 ctx.complete()
会输出到浏览器,但是,第一次 doGet 请求执行完成后,Tomcat 是怎么知道不用返回到客户端的呢?关键代码在 CoyoteAdapter
中的 service
方法,部分代码如下:
这部分代码在调用完 Servlet
后,会通过 request.isAsync()
来判断是否是异步请求,如果是异步请求,就设置 async = true
。如果是非异步请求就执行输出数据到客户端逻辑,同时销毁 request
和 response
。这里就完成了请求结束后不响应客户端的操作。
SpringMVC
的 DeferredResult
如何对接异步请求的
1.编写 DeferredResult 返回类型 api
2.接口调用
这样就完成了 DeferredResult 异步调用,当然我们也可以在 DeferredResult 设置超时相关逻辑。
3.原理与源码分析
为了方便理解,找了一张图来看一下 DeferredResult 做了什么事情。
- 接收到请求后,将请求暂存并且释放容器线程,用来接收新的请求
- 容器超时逻辑和业务正常处理逻辑将结果塞到 DeferredResult 返回调用
spring 对于 DeferredResult 请求处理
1.请求预处理
当然 DeferredResult 处理逻辑也脱离不了 spring mvc 的支持,也是要走到 DispatcherServlet
来处理请求:
对于支持 DeferredResult 异步处理逻辑有三个关键点:
- 生成异步管理器
- 执行异步处理逻辑
- 如果异步处理已经开始,返回调用
HandleAdapter#handle 会调用到 DeferredResultMethodReturnValueHandler 的 handleReturnValue 方法:
进入 WebAsyncManager
的 startDeferredResultProcessing
方法:
startAsyncProcessing 方法开启异步处理 (asyncManager.isConcurrentHandlingStarted 会用到),然后进入 DeferredResult 的 setResultHandler 设置结果处理器:
由于逻辑未处理,结果未设置,所以逻辑会走到设置结果处理器代码块,然后返回,此时返回值解析过程结束了,同时由于异步 servlet 的特性,tomcat 的连接也得到了释放。
预处理流程如下:
2.返回值处理
这个时候容器连接得到了释放,然而问题并没有解决,请求处理只完成了一半,业务处理返回值并没有真正返回。
我们在业务线程池处理调用了 DeferredResult 的 setResult 方法,最终会调用内部 setResultInternal:
这里会调用之前传入的函数式接口来处理:
然后调用 setConcurrentResultAndDispatch:
如果异步处理完成则返回调用,否则执行异步请求分发,该段代码执行完成会发起一次新的请求到后台,又被 DispatcherServlet 类接收到(但是不会再进入 controller 了),最终将结果响应给调用方。
tomcat 容器维度对异步支持
我们再从容器维度对 DeferredResult 异步请求的处理做一下分析,分别是请求超时和主动 setResult 返回。
1.请求超时
Connector 是 tomcat 的最核心的组件之一,主要的职责就是负责接收客户端连接和客户端请求的处理加工,初始化和启动会执行 Protocal 相关初始化和启动操作,看一下 AbstractProtocol 的启动:
public void start() throws Exception {
if (this.getLog().isInfoEnabled()) {
this.getLog().info(sm.getString("abstractProtocolHandler.start", new Object[]{this.getName()}));
this.logPortOffset();
}
this.endpoint.start();
this.monitorFuture = this.getUtilityExecutor().scheduleWithFixedDelay(() -> {
this.startAsyncTimeout();
}, 0L, 60L, TimeUnit.SECONDS);
}
延时 60 秒执行启动异步超时支持逻辑,调用 startAsyncTimeout:
异步请求会被放入 waitingProcessors 中,并且设置了超时时间,tomcat 会有一个线程每隔 1 秒遍历 waitingProcessors 里面的 processor,检查是否过期,如果过期会往 tomcat 线程池投掷超时事件:
线程池跑到这个任务的时候就知道这个已经超时请求任务,此时就会将超时值塞入到请求中,具体是通过之前设置的 DeferredResult 相关的拦截器中的 handleTimeout,比如 spring 自己提供的拦截器:
最终会把值放到管理异步请求 AsyncManager 中并重新下发请求交给 DispatcherServlet#doDispatch 处理,第二次进来的时候发现 AsyncManager 已经有值了,把结果进行包装然后直接返回调用了。
超时逻辑处理流程如下:
2.setResult 主动返回
业务线程在执行完逻辑,将结果塞回到 DeferredResult
时也会调用 setResultInternal,赋值完成后调用 AsyncWebRequest#dispatch
方法重新下发请求,DispatcherServlet 处理时发现 AsyncManager 已经有值了,封装后直接返回,后边逻辑和超时逻辑一样。
处理流程如下:
Tomcat 异步过滤器
Tomcat 创建过滤器的逻辑如下。
简单讲就是
- 匹配分发类型
- 匹配拦截的 URL
- 匹配过滤的 servlet
这里 matchDispatcher
在异步中会变成 javax.servlet.DispatcherType#ASYNC
,从而判断过滤器是否能够正常过滤。如果过滤器创建时,没有设置该类型,则无法匹配。
设置方案如下