天天看點

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 中。

Servlet3異步

接收到 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關系

  • 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);
        }
    }
}      

參考