大部分程序員還不知道的 Servelt3 異步請求,原來這么簡單?


前言

博文地址:https://sourl.cn/URptix

當一個 HTTP 請求到達 Tomcat,Tomcat 將會從線程池中取出線程,然后按照如下流程處理請求:

  • 將請求信息解析為 HttpServletRequest
  • 分發到具體 Servlet 處理相應的業務
  • 通過 HttpServletResponse 將響應結果返回給等待客戶端

整體流程如下所示:

這是我們日常最常用同步請求模型,所有動作都交給同一個 Tomcat 線程處理,所有動作處理完成,線程才會被釋放回線程池。

想象一下如果業務需要較長時間處理,那么這個 Tomcat 線程其實一直在被占用,隨着請求越來越多,可用 I/O 線程越來越少,直到被耗盡。這時后續請求只能等待空閑 Tomcat 線程,這將會加長了請求執行時間。

如果客戶端不關心返回業務結果,這時我們可以自定義線程池,將請求任務提交給線程池,然后立刻返回。

也可以使用 Spring Async 任務,大家感興趣可以自行查找一下資料

但是很多場景下,客戶端需要處理返回結果,我們沒辦法使用上面的方案。在 Servlet2 時代,我們沒辦法優化上面的方案。

不過等到 Servlet3 ,引入異步 Servelt 新特性,可以完美解決上面的需求。

異步 Servelt 執行請求流程:

  • 將請求信息解析為 HttpServletRequest
  • 分發到具體 Servlet 處理,將業務提交給自定義業務線程池,請求立刻返回,Tomcat 線程立刻被釋放
  • 當業務線程將任務執行結束,將會將結果轉交給 Tomcat 線程
  • 通過 HttpServletResponse 將響應結果返回給等待客戶端

引入異步 Servelt3 整體流程如下:

使用異步 Servelt,Tomcat 線程僅僅處理請求解析動作,所有耗時較長的業務操作全部交給業務線程池,所以相比同步請求, Tomcat 線程可以處理 更對請求。

雖然我們將業務處理交給業務線程池異步處理,但是對於客戶端來講,其還在同步等待響應結果

可能有些同學會覺得異步請求將會獲得更快響應時間,其實不是的,相反可能由於引入了更多線程,增加線程上下文切換時間。

雖然沒有降低響應時間,但是通過請求異步化帶來其他明顯優點

  • 可以處理更高並發連接數,提高系統整體吞吐量
  • 請求解析與業務處理完全分離,職責單一
  • 自定義業務線程池,我們可以更容易對其監控,降級等處理
  • 可以根據不同業務,自定義不同線程池,相互隔離,不用互相影響

所以具體使用過程,我們還需要進行的相應的壓測,觀察響應時間以及吞吐量等其他指標,綜合選擇。

異步 Servelt 使用方式

異步 Servelt 使用方式不是很難,小黑哥總結就是就是下面三板斧:

  1. HttpServletRequest#startAsync 獲取 AsyncContext 異步上下文對象
  2. 使用自定義的業務線程池處理業務邏輯
  3. 業務線程處理結束,通過 AsyncContext#complete 返回響應結果

下面的例子將會使用 SpringBoot ,Web 容器選擇 Tomcat

示例代碼如下:

ExecutorService executorService = Executors.newFixedThreadPool(10);

@RequestMapping("/hello")
public void hello(HttpServletRequest request) {
    AsyncContext asyncContext = request.startAsync();
    // 超時時間
    asyncContext.setTimeout(10000);
    executorService.submit(() -> {
        try {
            // 休眠 5s,模擬業務操作
            TimeUnit.SECONDS.sleep(5);
            // 輸出響應結果
            asyncContext.getResponse().getWriter().println("hello world");
            log.info("異步線程處理結束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            asyncContext.complete();
        }
    });
    log.info("servlet 線程處理結束");
}

瀏覽器訪問該請求將會同步等待 5s 得到輸出響應,應用日志輸出結果如下:

2020-03-24 07:27:08.997  INFO 79257 --- [nio-8087-exec-4] com.xxxx   : servlet 線程處理結束
2020-03-24 07:27:13.998  INFO 79257 --- [pool-1-thread-3] com.xxxx   : 異步線程處理結束

這里我們需要注意設置合理的超時時間,防止客戶端長時間等待。

SpringMVC

Servlet3 API ,無法使用 SpringMVC 為我們提供的特性,我們需要自己處理響應信息,處理方式相對繁瑣。

SpringMVC 3.2 基於 Servelt3 引入異步請求處理方式,我們可以跟使用同步請求一樣,方便使用異步請求。

SpringMVC 提供有兩種異步方式,只要將 Controller 方法返回值修改下述類即可:

  • DeferredResult
  • Callable

DeferredResult

DeferredResult 是 SpringMVC 3.2 之后引入新的類,只要讓請求方法返回 DeferredResult,就可以快速使用異步請求,示例代碼如下:

ExecutorService executorService = Executors.newFixedThreadPool(10);

@RequestMapping("/hello_v1")
public DeferredResult<String> hello_v1() {
    // 設置超時時間
    DeferredResult<String> deferredResult = new DeferredResult<>(7000L);
    // 異步線程處理結束,將會執行該回調方法
    deferredResult.onCompletion(() -> {
        log.info("異步線程處理結束");
    });
    // 如果異步線程執行時間超過設置超時時間,將會執行該回調方法
    deferredResult.onTimeout(() -> {
        log.info("異步線程超時");
        // 設置返回結果
        deferredResult.setErrorResult("timeout error");
    });
    deferredResult.onError(throwable -> {
        log.error("異常", throwable);
        // 設置返回結果
        deferredResult.setErrorResult("other error");
    });
    executorService.submit(() -> {
        try {
            TimeUnit.SECONDS.sleep(5);
            deferredResult.setResult("hello_v1");
            // 設置返回結果
        } catch (Exception e) {
            e.printStackTrace();
            // 若異步方法內部異常
            deferredResult.setErrorResult("error");
        }
    });
    log.info("servlet 線程處理結束");
    return deferredResult;

}

創建 DeferredResult 實例時可以傳入特定超時時間。另外我們可以設置默認超時時間:

# 異步請求超時時間
spring.mvc.async.request-timeout=2000

如果異步程序執行完成,可以調用 DeferredResult#setResult返回響應結果。此時若有設置 DeferredResult#onCompletion 回調方法,將會觸發該回調方法。

Go to implementation(s)

最后 DeferredResult 還提供其他異常的回調方法 onError,起初小黑哥以為只要異步線程內發生異常,就會觸發該回調方法。嘗試在異步線程內拋出異常,但是無法成功觸發。

后續小黑哥查看這個方法的 doc,當 web 容器線程處理異步請求是時發生異常,才能成功觸發。

image-20200326195610915

小黑哥不知道如何才能發生這個異常,有經驗的小伙伴們的可以留言告知下。

Callable

Spring 另外還提供一種異步請求使用方式,直接使用 JDK Callable。示例代碼如下:

@RequestMapping("/hello_v2")
public Callable<String> hello_v2() {
    return new Callable<String>() {
        @Override
        public String call() throws Exception {
            TimeUnit.SECONDS.sleep(5);
            log.info("異步方法結束");
            return "hello_v2";
        }
    };
}

默認情況下,直接執行將會輸出 WARN 日志:

image-20200326213122894

這是因為默認情況使用 SimpleAsyncTaskExecutor 執行異步請求,每次調用執行都將會新建線程。由於這種方式不復用線程,生產不推薦使用這種方式,所以我們需要使用線程池代替。

我們可以使用如下方式自定義線程池:

@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor executor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setThreadNamePrefix("test-");
    threadPoolTaskExecutor.setCorePoolSize(10);
    threadPoolTaskExecutor.setMaxPoolSize(20);
    return threadPoolTaskExecutor;
}

注意 Bean 名稱一定要是 applicationTaskExecutor,若不一致, Spring 將不會使用自定義線程池。

或者可以直接使用 SpringBoot 配置文件方式配置代替:

# 核心線程數
spring.task.execution.pool.core-size=10
# 最大線程數
spring.task.execution.pool.max-size=20
# 線程名前綴
spring.task.execution.thread-name-prefix=test
# 還有另外一些配置,讀者們可以自行配置

這種方式異步請求的超時時間只能通過配置文件方式配置。

spring.mvc.async.request-timeout=10000

如果需要為單獨請求的配置特定的超時時間,我們需要使用 WebAsyncTask 包裝 Callable

@RequestMapping("/hello_v3")
public WebAsyncTask<String> hello_v3() {
    System.out.println("asdas");
    Callable<String> callable=new Callable<String>() {
        @Override
        public String call() throws Exception {
            TimeUnit.SECONDS.sleep(5);
            log.info("異步方法結束");
            return "hello_v3";
        }
    };
    // 單位 ms
    WebAsyncTask<String> webAsyncTask=new WebAsyncTask<>(10000,callable);
    return webAsyncTask;
}

總結

SpringMVC 兩種異步請求方式,本質上就是幫我們包裝 Servlet3 API ,讓我們不用關心具體實現細節。雖然日常使用我們一般會選擇使用 SpringMVC 兩種異步請求方式,但是我們還是需要了解異步請求實際原理。所以大家如果在使用之前,可以先嘗試使用 Servlet3 API 練習,后續再使用 SpringMVC。

Reference

  1. https://www.baeldung.com/spring-deferred-result
  2. https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support

歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM