前言
通过我之前的Tomcat系列文章,相信看我博客的同学对Tomcat应该有一个比较清晰的了解了,在前几篇博客我们讨论了Tomcat在SpringBoot框架中是如何启动的,讨论了Tomcat的内部组件是如何设计以及请求是如何流转的,那么我们这篇博客聊聊Tomcat的异步Servlet,Tomcat是如何实现异步Servlet的以及异步Servlet的使用场景。
手撸一个异步的Servlet
我们直接借助SpringBoot框架来实现一个Servlet,这里只展示Servlet代码:
1 | "/async",asyncSupported = true) (urlPatterns = |
上面的代码实现了一个异步的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()
那我们来看看他们究竟做了什么?
1 | public AsyncContext startAsync(ServletRequest request, |
我们发现req.startAsync()
只是保存了一个异步上下文,同时设置一些基础信息,比如Timeout
,顺便提一下,这里设置的默认超时时间是30S,如果你的异步处理逻辑超过30S,此时执行ctx.complete()
就会抛出IllegalStateException 异常。
我们来看看ctx.complete()
的逻辑
1 | public void complete() { |
所以,这里最终会调用AbstractEndpoint
的processSocket
方法,之前看过我前面博客的同学应该有印象,EndPoint
是用来接受和处理请求的,接下来就会交给Processor
去进行协议处理。
1 | 类:AbstractProcessorLight |
这部分是重点,AbstractProcessorLight
会根据SocketEvent
的状态来判断是不是要去调用service(socketWrapper)
,该方法最终会去调用到容器,从而完成业务逻辑的调用,我们这个请求是执行完成后调用的,肯定不能进容器了,不然就是死循环了,这里通过isAsync()
判断,就会进入dispatch(status)
,最终会调用CoyoteAdapter
的asyncDispatch
方法
1 | public boolean asyncDispatch(org.apache.coyote.Request req, org.apache.coyote.Response res, |
上面的代码就是ctx.complete()
执行最终的方法了(当然省略了很多细节),完成了数据的输出,最终输出到浏览器。
这里有同学可能会说,我知道异步执行完后,调用ctx.complete()
会输出到浏览器,但是,第一次doGet请求执行完成后,Tomcat是怎么知道不用返回到客户端的呢?关键代码在CoyoteAdapter
中的service
方法,部分代码如下:
1 | postParseSuccess = postParseRequest(req, request, res, response); |
这部分代码在调用完Servlet
后,会通过request.isAsync()
来判断是否是异步请求,如果是异步请求,就设置async = true
。如果是非异步请求就执行输出数据到客户端逻辑,同时销毁request
和response
。这里就完成了请求结束后不响应客户端的操作。
为什么说Spring Boot的@EnableAsync注解不是异步Servlet
因为之前准备写本篇文章的时候就查询过很多资料,发现很多资料写SpringBoot异步编程都是依赖于@EnableAsync
注解,然后在Controller
用多线程来完成业务逻辑,最后汇总结果,完成返回输出。这里拿一个掘金大佬的文章来举例《新手也能看懂的 SpringBoot 异步编程指南》,这篇文章写得很通俗易懂,非常不错,从业务层面来说,确实是异步编程,但是有一个问题,抛开业务的并行处理来说,针对整个请求来说,并不是异步的,也就是说不能立即释放Tomcat的线程,从而不能达到异步Servlet的效果。这里我参考上文也写了一个demo,我们来验证下,为什么它不是异步的。
1 |
|
这里我运行下,看看效果
这里我请求之后,在调用容器执行业务逻辑之前打了一个断点,然后在返回之后的同样打了一个断点,在Controller
执行完之后,请求才回到了CoyoteAdapter
中,并且判断request.isAsync()
,根据图中看到,是为false
,那么接下来就会执行request.finishRequest()
和response.finishResponse()
来执行响应的结束,并销毁请求和响应体。很有趣的事情是,我实验的时候发现,在执行request.isAsync()
之前,浏览器的页面上已经出现了响应体,这是SpringBoot框架已经通过StringHttpMessageConverter
类中的writeInternal
方法已经进行输出了。
以上分析的核心逻辑就是,Tomcat的线程执行CoyoteAdapter
调用容器后,必须要等到请求返回,然后再判断是否是异步请求,再处理请求,然后执行完毕后,线程才能进行回收。而我一最开始的异步Servlet例子,执行完doGet方法后,就会立即返回,也就是会直接到request.isAsync()
的逻辑,然后整个线程的逻辑执行完毕,线程被回收。
聊聊异步Servlet的使用场景
分析了这么多,那么异步Servlet的使用场景有哪些呢?其实我们只要抓住一点就可以分析了,就是异步Servlet提高了系统的吞吐量,可以接受更多的请求。假设web系统中Tomcat的线程不够用了,大量请求在等待,而此时Web系统应用层面的优化已经不能再优化了,也就是无法缩短业务逻辑的响应时间了,这个时候,如果想让减少用户的等待时间,提高吞吐量,可以尝试下使用异步Servlet。
举一个实际的例子:比如做一个短信系统,短信系统对实时性要求很高,所以要求等待时间尽可能短,而发送功能我们实际上是委托运营商去发送的,也就是说我们要调用接口,假设并发量很高,那么这个时候业务系统调用我们的发送短信功能,就有可能把我们的Tomcat线程池用完,剩下的请求就会在队列中等待,那这个时候,短信的延时就上去了,为了解决这个问题,我们可以引入异步Servlet,接受更多的短信发送请求,从而减少短信的延时。
总结
这篇文章我从手写一个异步Servlet来开始,分析了异步Servlet的作用,以及Tomcat内部是如何实现异步Servlet的,然后我也根据互联网上流行的SpringBoot异步编程来进行说明,其在Tomcat内部并不是一个异步的Servlet。最后,我谈到了异步Servlet的使用场景,分析了什么情况下可以尝试异步Servlet。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。