博文地址:https://sourl.cn/URptixjava
當一個 HTTP 請求到達 Tomcat,Tomcat 將會從線程池中取出線程,而後按照以下流程處理請求:web
HttpServletRequest
HttpServletResponse
將響應結果返回給等待客戶端總體流程以下所示:spring
這是咱們平常最經常使用同步請求模型,全部動做都交給同一個 Tomcat 線程處理,全部動做處理完成,線程纔會被釋放回線程池。瀏覽器
想象一下若是業務須要較長時間處理,那麼這個 Tomcat 線程其實一直在被佔用,隨着請求愈來愈多,可用 I/O 線程愈來愈少,直到被耗盡。這時後續請求只能等待空閒 Tomcat 線程,這將會加長了請求執行時間。spring-mvc
若是客戶端不關心返回業務結果,這時咱們能夠自定義線程池,將請求任務提交給線程池,而後馬上返回。多線程
也可使用 Spring Async 任務,你們感興趣能夠自行查找一下資料併發
可是不少場景下,客戶端須要處理返回結果,咱們沒辦法使用上面的方案。在 Servlet2 時代,咱們沒辦法優化上面的方案。mvc
不過等到 Servlet3 ,引入異步 Servelt 新特性,能夠完美解決上面的需求。app
異步 Servelt 執行請求流程:異步
HttpServletRequest
Servlet
處理,將業務提交給自定義業務線程池,請求馬上返回,Tomcat 線程馬上被釋放HttpServletResponse
將響應結果返回給等待客戶端引入異步 Servelt3 總體流程以下:
使用異步 Servelt,Tomcat 線程僅僅處理請求解析動做,全部耗時較長的業務操做所有交給業務線程池,因此相比同步請求, Tomcat 線程能夠處理 更對請求。
雖然咱們將業務處理交給業務線程池異步處理,可是對於客戶端來說,其還在同步等待響應結果。
可能有些同窗會以爲異步請求將會得到更快響應時間,其實不是的,相反可能因爲引入了更多線程,增長線程上下文切換時間。
雖然沒有下降響應時間,可是經過請求異步化帶來其餘明顯優勢:
因此具體使用過程,咱們還須要進行的相應的壓測,觀察響應時間以及吞吐量等其餘指標,綜合選擇。
異步 Servelt 使用方式不是很難,小黑哥總結就是就是下面三板斧:
HttpServletRequest#startAsync
獲取 AsyncContext
異步上下文對象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 : 異步線程處理結束
這裏咱們須要注意設置合理的超時時間,防止客戶端長時間等待。
Servlet3 API ,沒法使用 SpringMVC 爲咱們提供的特性,咱們須要本身處理響應信息,處理方式相對繁瑣。
SpringMVC 3.2 基於 Servelt3 引入異步請求處理方式,咱們能夠跟使用同步請求同樣,方便使用異步請求。
SpringMVC 提供有兩種異步方式,只要將 Controller
方法返回值修改下述類便可:
DeferredResult
Callable
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
回調方法,將會觸發該回調方法。
最後 DeferredResult
還提供其餘異常的回調方法 onError
,起初小黑哥覺得只要異步線程內發生異常,就會觸發該回調方法。嘗試在異步線程內拋出異常,可是沒法成功觸發。
後續小黑哥查看這個方法的 doc,當 web 容器線程處理異步請求是時發生異常,才能成功觸發。
小黑哥不知道如何才能發生這個異常,有經驗的小夥伴們的能夠留言告知下。
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 日誌:
這是由於默認狀況使用 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。
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客:studyidea.cn