Tomcat是如何实现异步Servlet的( 三 )


为什么说Spring Boot的@EnableAsync注解不是异步Servlet因为之前准备写本篇文章的时候就查询过很多资料 , 发现很多资料写SpringBoot异步编程都是依赖于@EnableAsync注解 , 然后在Controller用多线程来完成业务逻辑 , 最后汇总结果 , 完成返回输出 。这里拿一个掘金大佬的文章来举例《新手也能看懂的 SpringBoot 异步编程指南》 , 这篇文章写得很通俗易懂 , 非常不错 , 从业务层面来说 , 确实是异步编程 , 但是有一个问题 , 抛开业务的并行处理来说 , 针对整个请求来说 , 并不是异步的 , 也就是说不能立即释放Tomcat的线程 , 从而不能达到异步Servlet的效果 。这里我参考上文也写了一个demo , 我们来验证下 , 为什么它不是异步的 。
@RestController@Slf4jpublic class TestController { @Autowired private TestService service; @GetMapping("/hello") public String test() { try { log.info("testAsynch Start"); CompletableFuture<String> test1 = service.test1(); CompletableFuture<String> test2 = service.test2(); CompletableFuture<String> test3 = service.test3(); CompletableFuture.allOf(test1, test2, test3); log.info("test1=====" + test1.get()); log.info("test2=====" + test2.get()); log.info("test3=====" + test3.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return "hello"; }@Servicepublic class TestService { @Async("asyncExecutor") public CompletableFuture<String> test1() throws InterruptedException { Thread.sleep(3000L); return CompletableFuture.completedFuture("test1"); } @Async("asyncExecutor") public CompletableFuture<String> test2() throws InterruptedException { Thread.sleep(3000L); return CompletableFuture.completedFuture("test2"); } @Async("asyncExecutor") public CompletableFuture<String> test3() throws InterruptedException { Thread.sleep(3000L); return CompletableFuture.completedFuture("test3"); }}@SpringBootApplication@EnableAsyncpublic class TomcatdebugApplication { public static void main(String[] args) { SpringApplication.run(TomcatdebugApplication.class, args); } @Bean(name = "asyncExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); executor.setMaxPoolSize(3); executor.setQueueCapacity(100); executor.setThreadNamePrefix("AsynchThread-"); executor.initialize(); return executor; }这里我运行下 , 看看效果

Tomcat是如何实现异步Servlet的

文章插图
这里我请求之后 , 在调用容器执行业务逻辑之前打了一个断点 , 然后在返回之后的同样打了一个断点 , 在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,接受更多的短信发送请求 , 从而减少短信的延时 。


推荐阅读