一、什么是异步Servlet
当一个新的请求到达时,Tomcat会从线程池里拿出一个线程来处理请求,这个线程会调用你的Web应用,Web应用在处理请求的过程中,Tomcat线程会一直阻塞,直到Web应用处理完毕才能再输出响应,最后Tomcat才回收这个线程
假如你的Web应用需要较长的时间来处理请求(比如数据库查询或者等待下游的服务调用返回),那么Tomcat线程一直不回收,会占用系统资源,在极端情况下会导致线程饥饿,也就是说Tomcat没有更多的线程来处理新的请求
那该如何解决这个问题呢?
Servlet3.0中引入的异步Servlet。主要是在Web应用里启动一个单独的线程来执行这些比较耗时的请求,而Tomcat线程立即返回,不再等待Web应用将请求处理完,这样Tomcat线程可以立即被回收到线程池,用来响应其他请求,降低了系统的资源消耗,同时还能提高系统的吞吐量
二、异步Servlet示例
SpringBoot启动类添加@ServletComponentScan
注解,扫描Servlet
@ServletComponentScan
@SpringBootApplication
public class AsyncServletApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncServletApplication.class, args);
}
}
异步Servlet:
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
//Web应用线程池,用来处理异步Servlet
ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) {
//调用startAsync或者异步上下文
AsyncContext asyncContext = req.startAsync();
//添加AsyncListener
asyncContext.addListener(new AsyncServletListener());
//用线程池来执行耗时操作
executor.execute(new Runnable() {
@Override
public void run() {
//在这里做耗时的操作
try {
asyncContext.getResponse().getWriter().println("Handling Async Servlet");
} catch (IOException e) {
}
//异步Servlet处理完了调用异步上下文的complete方法
asyncContext.complete();
}
});
}
}
异步Servlet监听:
public class AsyncServletListener implements AsyncListener {
private static final Logger LOGGER = LoggerFactory.getLogger(AsyncServletListener.class);
/**
* 异步线程执行完毕时回调
*
* @param asyncEvent
* @throws IOException
*/
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
LOGGER.info("AsyncServlet onComplete");
}
/**
* 异步线程执行超时回调
*
* @param asyncEvent
* @throws IOException
*/
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
LOGGER.info("AsyncServlet onTimeout");
}
/**
* 异步线程执行出错回调
*
* @param asyncEvent
* @throws IOException
*/
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
LOGGER.info("AsyncServlet onError");
}
/**
* 异步线程开始执行时回调
*
* @param asyncEvent
* @throws IOException
*/
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
LOGGER.info("AsyncServlet onStartAsync");
}
}
上面的代码有三个要点:
-
通过注解的方式来注册Servlet,除了@WebServlet注解,还需要加上asyncSupported=true的属性,表明当前的Servlet是一个异步Servlet
-
Web应用程序需要调用Request对象的
startAsync()
方法来拿到一个异步上下文AsyncContext。这个上下文保存了请求和响应对象 -
Web应用需要开启一个新线程来处理耗时的操作,处理完成后需要调用AsyncContext的
complete()
方法。目的是告诉Tomcat,请求已经处理完成
虽然异步Servlet允许用更长的时间来处理请求,但是也有超时限制的,默认是30秒,如果30秒内请求还没处理完,Tomcat会触发超时机制,向浏览器返回超时错误,如果这个时候你的Web应用再调用asyncContext.complete()
方法,会得到一个IllegalStateException异常
三、异步Servlet原理
接收到Request请求之后,由Tomcat工作线程从HttpServletRequest中获得一个异步上下文AsyncContext对象,然后由Tomcat工作线程把AsyncContext对象传递给业务处理线程,同时Tomcat工作线程归还到工作线程池,这一步就是异步开始。在业务处理线程中完成业务逻辑的处理,生成response返回给客户端
在Servlet3.0中虽然处理请求可以实现异步,但是InputStream和OutputStream的IO操作还是阻塞的,当数据量大的Request Body或者Response Body的时候,就会导致不必要的等待。从Servlet3.1以后增加了非阻塞IO,需要Tomcat8.x支持,通过在HttpServletRequest和HttpServletResponse中分别添加ReadListener和WriterListener方式,只有在IO数据满足一定条件时(比如数据准备好时),才进行后续的操作
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
//Web应用线程池,用来处理异步Servlet
ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//调用startAsync或者异步上下文
AsyncContext asyncContext = req.startAsync();
//添加AsyncListener
asyncContext.addListener(new AsyncServletListener());
ServletInputStream inputStream = req.getInputStream();
inputStream.setReadListener(new ReadListener() {
@Override
public void onDataAvailable() throws IOException {
}
@Override
public void onAllDataRead() throws IOException {
//用线程池来执行耗时操作
executor.execute(new Runnable() {
@Override
public void run() {
//在这里做耗时的操作
try {
asyncContext.getResponse().getWriter().println("Handling Async Servlet");
} catch (IOException e) {
}
//异步Servlet处理完了调用异步上下文的complete方法
asyncContext.complete();
}
});
}
@Override
public void onError(Throwable throwable) {
}
});
}
}
参考:
https://time.geekbang.org/column/article/106935
https://blog.csdn.net/wangxindong11/article/details/78591396
https://www.cnblogs.com/davenkin/p/async-servlet.html