Servlet3异步

概述

在 Servlet3.0 之前,Servlet 采用 Thread-Per-Request 的方式处理 Http 请求,即每一次请求都是由某一个线程从头到尾负责处理。

如果一个请求需要进行 IO 操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待 IO 操作完成, 而 IO 操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用,如果并发量很大的话,那肯定会造性能问题。

传统的 MVC 框架如 SpringMVC 也无法摆脱 Servlet 的桎梏,他们都是基于 Servlet 来实现的。

为了解决这一问题,Servlet3.0引入异步 Servlet,Servlet3.1引入非阻塞 IO 来进一步增强异步处理的性能

引申

同步异步是数据通信的方式,阻塞和非阻塞是一种状态。比如同步这种数据通讯方式里面可以有阻塞状态也可以有非阻塞状态。从另外一个角度理解同步和异步,就是如果一个线程干完的事情都是同步,有线程切换才能干完的事情就是异步。

版本

Servlet 和 Tomcat的对应关系,用错 Tomcat 版本可能就不支持异步Servlet。参考tomcat官网,Servlet3.0 对应的 Tomcat 版本是 7.0.x,Servlet3.1 对应的 Tomcat 版本是 8.0.x。

入门

同步Servlet

先来看同步Servlet:

@Slf4j
@WebServlet(urlPatterns = "/sync")
public class SyncServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long start = System.currentTimeMillis();
        printLog(request, response);
        log.info("总耗时:" + (System.currentTimeMillis() - start));
    }

    private void printLog(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        response.getWriter().write("ok");
    }
}

前端发送请求,最终 doGet 方法中耗时 3001 毫秒。在整个请求处理过程中,请求会一直占用 Servlet 线程,直到一个请求处理完毕这个线程才会被释放。

异步

直接把 printLog 方法扔到子线程里边去执行就是异步吗?但是这样会有另外一个问题,子线程里边没有办法通过 HttpServletResponse 直接返回数据,所以一定需要 Servlet 的异步支持,然后才可以在子线程中返回数据。

@Slf4j
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long start = System.currentTimeMillis();
        AsyncContext asyncContext = request.startAsync();
        CompletableFuture.runAsync(() -> printLog(asyncContext,asyncContext.getRequest(),asyncContext.getResponse()));
        log.info("总耗时:" + (System.currentTimeMillis() - start));
    }

    private void printLog(AsyncContext asyncContext, ServletRequest request, ServletResponse response){
        try {
            Thread.sleep(3000);
            response.getWriter().write("ok");
            asyncContext.complete();
        } catch (InterruptedException | IOException e) {
        }
    }
}

改造主要有如下几方面:

  • @WebServlet 注解上添加 asyncSupported 属性,开启异步支持
  • 调用request.startAsync();开启异步上下文
  • 通过 JDK8 中的CompletableFuture.runAsync();来启动一个子线程(当然也可以自己 new 一个子线程)
  • 调用 printLog 方法时的 request 和 response 重新构造,直接从 asyncContext 中获取
  • 在 printLog 方法中,方法执行完成后,调用asyncContext.complete()通知异步上下文请求处理完毕。

有异步 Servlet后,后台 Servlet 的线程会被及时释放,释放之后又可以去接收新的请求,进而提高应用的并发能力。

深入

Servlet3 的异步使用步骤

步骤:

  • 声明 Servlet,增加 asyncSupported 属性,开启异步支持:@WebServlet(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
  • 通过 request 获取异步上下文 AsyncContext:AsyncContext asyncCtx = request.startAsync();
  • 开启业务逻辑处理线程,并将 AsyncContext 传递给业务线程:executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
  • 在异步业务逻辑处理线程中,通过 asyncContext 获取 request 和 response,处理对应的业务
  • 业务逻辑处理线程处理完成逻辑之后,调用AsyncContext.complete方法:asyncContext.complete();结束该次异步线程处理

Servlet3异步流程

在tomcat的组件中 Connector 和 Engine 是最核心的两个,Servlet3 的异步处理就是发生在 Connector 中。
在这里插入图片描述
接收到 request 请求之后,由 tomcat 工作线程从 HttpServletRequest 中获得一个异步上下文 AsyncContext 对象,然后由 tomcat 工作线程把 AsyncContext 对象传递给业务处理线程,同时 tomcat 工作线程归还到工作线程池,这一步就是异步开始。在业务处理线程中完成业务逻辑的处理,生成 response 返回给客户端。在 Servlet3.0 中虽然处理请求可以实现异步,但是 InputStream 和 OutputStream 的 IO 操作还是阻塞的,当数据量大的 request body 或者 response body时,就会导致不必要的等待。 Servlet3.1+ 增加非阻塞 IO。

Tomcat NIO Connector,Servlet 3.0 Async,Spring MVC Async关系

NIO是一种 IO模型,对比于BIO,它可以利用较少的线程处理更多的连接从而增加机器的吞吐量,Tomcat NIO Connector 是 Tomcat 的一种 NIO 连接模式。异步,前面提到他是一种通讯的方式,它跟 NIO 没有任务关系,即使没有 NIO 也可以实现异步,Servlet 3.0 Async 是指 Servlet 3 规范以后支持异步处理 Servlet 请求,可以把请求线程和业务线程分开。Spring MVC Async 是在 Servlet3 异步的基础上的封装。具体的区别如下:

  • Tomcat NIO Connector
    Tomcat 的 Connector 有三种模式,BIO,NIO,APR,Tomcat NIO Connector 是其中的 NIO 模式,使得 tomcat 容器可以用较少的线程处理大量的连接请求,不再是传统的一请求一线程模式。Tomcat 的 server.xml 配置 protocol="org.apache.coyote.http11.Http11NioProtocol",Http11NioProtocol 从 tomcat 6.x 开始支持。
  • Servlet 3.0 Async
    是说 Servlet 3.0 支持业务请求的异步处理,Servlet3 之前一个请求的处理流程,请求解析、READ BODY、RESPONSE BODY、以及其中的业务逻辑处理都由 Tomcat 线程池中的一个线程进行处理的。3.0 以后可以让请求线程(IO 线程)和业务处理线程分开,进而对业务进行线程池隔离。还可以根据业务重要性进行业务分级,然后再把线程池分级。还可以根据这些分级做其它操作比如监控和降级处理。
  • Spring MVC Async
    是 Spring MVC 3.2+ 基于 Servlet 3 的基础做的封装,原理及实现方式同上,使用:
@RestController
@RequestMapping("/async")
public class TestController {
    @RequestMapping("/{testUrl}")
    public DeferredResult<ResponseEntity<String>> testProcess(@PathVariable String testUrl) {
        final DeferredResult<ResponseEntity<String>> deferredResult = new DeferredResult<ResponseEntity<String>>();
        // 业务逻辑异步处理,将处理结果 set 到 DeferredResult
        new Thread(new AsyncTask(deferredResult)).start();
        return deferredResult;
    }

	@AllArgsConstructor
    private static class AsyncTask implements Runnable {
        private DeferredResult result;

        @Override
        public void run() {
            //业务逻辑START
            //...
            //业务逻辑END
            result.setResult(result);
        }
    }
}

参考

Servlet3 异步原理与实践
异步Servlet都不懂,谈何 WebFlux?

posted @ 2021-09-21 16:12  johnny233  阅读(35)  评论(0编辑  收藏  举报  来源