大部分程序員不知道的 Servelt3 異步請求,原來這么簡單?
用同步請求模型,所有動作都交給同一個 Tomcat 線程處理,所有動作處理完成,線程才會被釋放回線程池。
想象一下如果業(yè)務(wù)需要較長時間處理,那么這個 Tomcat 線程其實一直在被占用,隨著請求越來越多,可用 I/O 線程越來越少,直到被耗盡。這時后續(xù)請求只能等待空閑 Tomcat 線程,這將會加長了請求執(zhí)行時間。
如果客戶端不關(guān)心返回業(yè)務(wù)結(jié)果,這時我們可以自定義線程池,將請求任務(wù)提交給線程池,然后立刻返回。
也可以使用 Spring Async 任務(wù),大家感興趣可以自行查找一下資料
但是很多場景下,客戶端需要處理返回結(jié)果,我們沒辦法使用上面的方案。在 Servlet2 時代,我們沒辦法優(yōu)化上面的方案。
不過等到 Servlet3 ,引入異步 Servlet 新特性,可以完美解決上面的需求。
異步 Servlet 執(zhí)行請求流程:
將請求信息解析為 HttpServletRequest
分發(fā)到具體 Servlet 處理,將業(yè)務(wù)提交給自定義業(yè)務(wù)線程池,請求立刻返回,Tomcat 線程立刻被釋放
當(dāng)業(yè)務(wù)線程將任務(wù)執(zhí)行結(jié)束,將會將結(jié)果轉(zhuǎn)交給 Tomcat 線程
通過 HttpServletResponse 將響應(yīng)結(jié)果返回給等待客戶端
引入異步 Servlet3 整體流程如下:
使用異步 Servelt,Tomcat 線程僅僅處理請求解析動作,所有耗時較長的業(yè)務(wù)操作全部交給業(yè)務(wù)線程池,所以相比同步請求, Tomcat 線程可以處理 更多請求。
雖然我們將業(yè)務(wù)處理交給業(yè)務(wù)線程池異步處理,但是對于客戶端來講,其還在同步等待響應(yīng)結(jié)果。
可能有些同學(xué)會覺得異步請求將會獲得更快響應(yīng)時間,其實不是的,相反可能由于引入了更多線程,增加線程上下文切換時間。
雖然沒有降低響應(yīng)時間,但是通過請求異步化帶來其他明顯優(yōu)點(diǎn):
- 可以處理更高并發(fā)連接數(shù),提高系統(tǒng)整體吞吐量
- 請求解析與業(yè)務(wù)處理完全分離,職責(zé)單一
- 自定義業(yè)務(wù)線程池,我們可以更容易對其監(jiān)控,降級等處理
- 可以根據(jù)不同業(yè)務(wù),自定義不同線程池,相互隔離,不用互相影響
所以具體使用過程,我們還需要進(jìn)行的相應(yīng)的壓測,觀察響應(yīng)時間以及吞吐量等其他指標(biāo),綜合選擇。
異步 Servelt 使用方式
異步 Servelt 使用方式不是很難,阿粉總結(jié)就是下面三板斧:
- HttpServletRequest#startAsync 獲取 AsyncContext 異步上下文對象
- 使用自定義的業(yè)務(wù)線程池處理業(yè)務(wù)邏輯
- 業(yè)務(wù)線程處理結(jié)束,通過 AsyncContext#complete 返回響應(yīng)結(jié)果
下面的例子將會使用 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,模擬業(yè)務(wù)操作
- TimeUnit.SECONDS.sleep(5);
- // 輸出響應(yīng)結(jié)果
- asyncContext.getResponse().getWriter().println("hello world");
- log.info("異步線程處理結(jié)束");
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- asyncContext.complete();
- }
- });
- log.info("servlet 線程處理結(jié)束");
- }
瀏覽器訪問該請求將會同步等待 5s 得到輸出響應(yīng),應(yīng)用日志輸出結(jié)果如下:
- 2020-03-24 07:27:08.997 INFO 79257 --- [nio-8087-exec-4] com.xxxx : servlet 線程處理結(jié)束
- 2020-03-24 07:27:13.998 INFO 79257 --- [pool-1-thread-3] com.xxxx : 異步線程處理結(jié)束
這里我們需要注意設(shè)置合理的超時時間,防止客戶端長時間等待。
SpringMVC
Servlet3 API ,無法使用 SpringMVC 為我們提供的特性,我們需要自己處理響應(yīng)信息,處理方式相對繁瑣。
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() {
- // 設(shè)置超時時間
- DeferredResult<String> deferredResult = new DeferredResult<>(7000L);
- // 異步線程處理結(jié)束,將會執(zhí)行該回調(diào)方法
- deferredResult.onCompletion(() -> {
- log.info("異步線程處理結(jié)束");
- });
- // 如果異步線程執(zhí)行時間超過設(shè)置超時時間,將會執(zhí)行該回調(diào)方法
- deferredResult.onTimeout(() -> {
- log.info("異步線程超時");
- // 設(shè)置返回結(jié)果
- deferredResult.setErrorResult("timeout error");
- });
- deferredResult.onError(throwable -> {
- log.error("異常", throwable);
- // 設(shè)置返回結(jié)果
- deferredResult.setErrorResult("other error");
- });
- executorService.submit(() -> {
- try {
- TimeUnit.SECONDS.sleep(5);
- deferredResult.setResult("hello_v1");
- // 設(shè)置返回結(jié)果
- } catch (Exception e) {
- e.printStackTrace();
- // 若異步方法內(nèi)部異常
- deferredResult.setErrorResult("error");
- }
- });
- log.info("servlet 線程處理結(jié)束");
- return deferredResult;
- }
創(chuàng)建 DeferredResult 實例時可以傳入特定超時時間。另外我們可以設(shè)置默認(rèn)超時時間:
- # 異步請求超時時間
- spring.mvc.async.request-timeout=2000
如果異步程序執(zhí)行完成,可以調(diào)用 DeferredResult#setResult返回響應(yīng)結(jié)果。此時若有設(shè)置 DeferredResult#onCompletion 回調(diào)方法,將會觸發(fā)該回調(diào)方法。
同時我們還可以設(shè)置超時回調(diào)方法 DeferredResult#onTimeout,一旦異步線程執(zhí)行超時,將會觸發(fā)該回調(diào)方法。
最后 DeferredResult 還提供其他異常的回調(diào)方法 onError,起初阿粉以為只要異步線程內(nèi)發(fā)生異常,就會觸發(fā)該回調(diào)方法。嘗試在異步線程內(nèi)拋出異常,但是無法成功觸發(fā)。
后續(xù)阿粉查看這個方法的 doc,當(dāng) web 容器線程處理異步請求時發(fā)生異常,才能成功觸發(fā)。
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("異步方法結(jié)束");
- return "hello_v2";
- }
- };
- }
默認(rèn)情況下,直接執(zhí)行將會輸出 WARN 日志
這是因為默認(rèn)情況使用 SimpleAsyncTaskExecutor 執(zhí)行異步請求,每次調(diào)用執(zhí)行都將會新建線程。由于這種方式不復(fù)用線程,生產(chǎn)不推薦使用這種方式,所以我們需要使用線程池代替。
我們可以使用如下方式自定義線程池:
- @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 配置文件方式配置代替:
- # 核心線程數(shù)
- spring.task.execution.pool.core-size=10
- # 最大線程數(shù)
- spring.task.execution.pool.max-size=20
- # 線程名前綴
- spring.task.execution.thread-name-prefix=test
- # 還有另外一些配置,讀者們可以自行配置
這種方式異步請求的超時時間只能通過配置文件方式配置。
- spring.mvc.async.request-timeout=10000
如果需要為單獨(dú)請求的配置特定的超時時間,我們需要使用 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("異步方法結(jié)束");
- return "hello_v3";
- }
- };
- // 單位 ms
- WebAsyncTask<String> webAsyncTask=new WebAsyncTask<>(10000,callable);
- return webAsyncTask;
- }
總結(jié)
SpringMVC 兩種異步請求方式,本質(zhì)上就是幫我們包裝 Servlet3 API ,讓我們不用關(guān)心具體實現(xiàn)細(xì)節(jié)。雖然日常使用我們一般會選擇使用 SpringMVC 兩種異步請求方式,但是我們還是需要了解異步請求實際原理。所以大家如果在使用之前,可以先嘗試使用 Servlet3 API 練習(xí),后續(xù)再使用 SpringMVC。
- Referencehttps://www.baeldung.com/spring-deferred-result
- https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support