背景
tomcat 会从线程池中拿线程调用 Servlet 的 Service 方法来处理请求。
Web 应用可能需要较长时间来处理请求(数据库查询、下游的服务调用)
Service 方法阻塞会导致线程阻塞,无法及时回收,导致并发请求数量降低。
解决方案
Servlet 3.0 中引入的异步 Servlet。主要是在 Web 应用里启动一个单独的线程来执行这些比较耗时的请求,而 Tomcat 线程不再等待 Web 应用将请求处理完,立即返回,被回收到线程池。
何时使用异步 Servlet ?
如果发现 Tomcat 的线程不够了,大量线程阻塞在等待 Web 应用的处理上,而 Web 应用又没有优化的空间了,确实需要长时间处理,这个时候你不妨尝试一下异步 Servlet。
异步 Servlet 的设计思想
异步 Servlet 将 Tomcat 线程和 Web 应用线程分开,体现了隔离的思想,也就是把不同的业务处理所使用的资源隔离开,使得它们互不干扰,减少了线程的阻塞等待。
异步 Servlet 实现
代码上,异步 Servlet 继承了 HttpServlet,重写了 Service 方法。
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
//Web应用线程池,用来处理异步Servlet
ExecutorService executor = Executors.newSingleThreadExecutor();
public void service(HttpServletRequest req, HttpServletResponse resp) {
//1. 调用startAsync或者异步上下文
final AsyncContext ctx = req.startAsync();
//用线程池来执行耗时操作
executor.execute(new Runnable() {
@Override
public void run() {
//在这里做耗时的操作
try {
ctx.getResponse().getWriter().println("Handling Async Servlet");
} catch (IOException e) {}
//3. 异步Servlet处理完了调用异步上下文的complete方法
ctx.complete();
}
});
}
}
req.startAsync 方法
Web 应用线程调用 Request 对象的 startAsync 方法来拿到一个异步上下文 AsyncContext 对象,用来保存请求的中间信息,比如 Request 和 Response 对象等上下文信息。
为什么需要保存这些信息呢?
因为 Tomcat 的工作线程在 request.startAsync调用之后,就直接结束回到线程池中了,线程本身不会保存任何信息。也就是说一个请求到服务端,执行到一半,你的 Web 应用正在处理,这个时候 Tomcat 的工作线程没了,这就需要有个缓存能够保存原始的 Request 和 Response 对象,而这个缓存就是 AsyncContext。
有了 AsyncContext,Web 应用通过它拿到 Request 和 Response 对象,拿到 Request 对象后就可以读取请求信息,请求处理完了还需要通过 Response 对象将 HTTP 响应发送给浏览器。
连接器是调用 CoyoteAdapter 的 service 方法来处理请求的,而 CoyoteAdapter 会调用容器的 service 方法,当容器的 service 方法返回时,CoyoteAdapter 判断当前的请求是不是异步 Servlet 请求,如果是,就不会销毁 Request 和 Response 对象,也不会把响应信息发到浏览器。
CoyoteAdapter 的 service 方法,对它进行了简化:
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
//调用容器的service方法处理请求
connector.getService().getContainer().getPipeline().
getFirst().invoke(request, response);
//如果是异步Servlet请求,仅仅设置一个标志,
//否则说明是同步Servlet请求,就将响应数据刷到浏览器
if (request.isAsync()) {
async = true;
} else {
request.finishRequest();
response.finishResponse();
}
//如果不是异步Servlet请求,就销毁Request对象和Response对象
if (!async) {
request.recycle();
response.recycle();
}
}
ctx.complete方法
当请求处理完成时,Web 应用调用这个方法。那么这个方法做了些什么事情呢?最重要的就是把响应数据发送到浏览器。
这件事情不能由 Web 应用线程来做,也就是说 ctx.complete方法不能直接把响应数据发送到浏览器,因为这件事情应该由 Tomcat 线程来做,但具体怎么做呢?
连接器中的 Endpoint 组件检测到有请求数据达到时,会创建一个 SocketProcessor 对象交给线程池去处理,因此 Endpoint 的通信处理和具体请求处理在两个线程里运行。
在异步 Servlet 的场景里,Web 应用通过调用ctx.complete方法时,也可以生成一个新的 SocketProcessor 类,交给线程池处理。
对于异步 Servlet 请求来说,相应的 Socket 和协议处理组件 Processor 都被缓存起来了,并且这些对象都可以通过 Request 对象拿到。