【SpringBoot WEB系列】非同步請求知識點與使用姿勢小結
在 Servlet3.0 就引入了非同步請求的支援,但是在實際的業務開發中,可能用過這個特性的童鞋並不多?
本篇博文作為非同步請求的掃盲和使用教程,將包含以下知識點
- 什麼是非同步請求,有什麼特點,適用場景
-
四種使用姿勢:
- AsyncContext 方式
- Callable
- WebAsyncTask
- DeferredResult
<!-- more -->
I. 非同步請求
非同步對於我們而言,應該屬於經常可以聽到的詞彙了,在實際的開發中多多少少都會用到,那麼什麼是非同步請求呢
1. 非同步請求描述
先介紹一下同步與非同步:
一個正常呼叫,吭哧吭哧執行完畢之後直接返回,這個叫同步;
接收到呼叫,自己不幹,新開一個執行緒來做,主執行緒自己則去幹其他的事情,等後臺執行緒吭哧吭哧的跑完之後,主執行緒再返回結果,這個就叫非同步
非同步請求:
我們這裡講到的非同步請求,主要是針對 web 請求而言,後端響應請求的一種手段,同步/非同步對於前端而言是無感知、無區別的
同步請求,後端接收到請求之後,直接在處理請求執行緒中,執行業務邏輯,並返回
非同步請求,後端接收到請求之後,新開一個執行緒,來執行業務邏輯,釋放請求執行緒,避免請求執行緒被大量耗時的請求沾滿,導致服務不可用
2. 特點
透過上面兩張圖,可以知道非同步請求的最主要特點
- 業務執行緒,處理請求邏輯
- 請求處理執行緒立即釋放,透過回撥處理執行緒返回結果
3. 場景分析
從特點出發,也可以很容易看出非同步請求,更適用於耗時的請求,快速的釋放請求處理執行緒,避免 web 容器的請求執行緒被打滿,導致服務不可用
舉一個稍微極端一點的例子,比如我以前做過的一個多媒體服務,提供圖片、音影片的編輯,這些服務介面有同步返回結果的也有非同步返回結果的;同步返回結果的介面有快有慢,大部分耗時可能<10ms
,而有部分介面耗時則在幾十甚至上百
這種場景下,耗時的介面就可以考慮用非同步請求的方式來支援了,避免佔用過多的請求處理執行緒,影響其他的服務
II. 使用姿勢
接下來介紹四種非同步請求的使用姿勢,原理一致,只是使用的場景稍有不同
1. AsyncContext
在 Servlet3.0+之後就支援了非同步請求,第一種方式比較原始,相當於直接藉助 Servlet 的規範來實現,當然下面的 case 並不是直接建立一個 servlet,而是藉助AsyncContext
來實現
@RestController
@RequestMapping(path = "servlet")
public class ServletRest {
@GetMapping(path = "get")
public void get(HttpServletRequest request) {
AsyncContext asyncContext = request.startAsync();
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("操作完成:" + Thread.currentThread().getName());
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("超時返回!!!");
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("超時了!!!!");
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("出現了m某些異常");
asyncEvent.getThrowable().printStackTrace();
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("出現了某些異常哦!!!!");
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("開始執行");
}
});
asyncContext.setTimeout(3000L);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(Long.parseLong(request.getParameter("sleep")));
System.out.println("內部執行緒:" + Thread.currentThread().getName());
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("非同步返回!");
asyncContext.getResponse().getWriter().flush();
// 非同步完成,釋放
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
System.out.println("主執行緒over!!! " + Thread.currentThread().getName());
}
}
完整的實現如上,簡單的來看一下一般步驟
-
javax.servlet.ServletRequest#startAsync()
獲取AsyncContext
-
新增監聽器
asyncContext.addListener(AsyncListener)
(這個是可選的)- 使用者請求開始、超時、異常、完成時回撥
- 設定超時時間
asyncContext.setTimeout(3000L)
(可選) - 非同步任務
asyncContext.start(Runnable)
2. Callable
相比較於上面的複雜的示例,SpringMVC 可以非常 easy 的實現,直接返回一個Callable
即可
@RestController
@RequestMapping(path = "call")
public class CallableRest {
@GetMapping(path = "get")
public Callable<String> get() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("do some thing");
Thread.sleep(1000);
System.out.println("執行完畢,返回!!!");
return "over!";
}
};
return callable;
}
@GetMapping(path = "exception")
public Callable<String> exception() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("do some thing");
Thread.sleep(1000);
System.out.println("出現異常,返回!!!");
throw new RuntimeException("some error!");
}
};
return callable;
}
}
請注意上面的兩種 case,一個正常返回,一個業務執行過程中,丟擲來異常
分別請求,輸出如下
# http://localhost:8080/call/get
do some thing
執行完畢,返回!!!
異常請求: http://localhost:8080/call/exception
do some thing
出現異常,返回!!!
2020-03-29 16:12:06.014 ERROR 24084 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.RuntimeException: some error!
at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:40) ~[classes/:na]
at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:34) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:328) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171]
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) ~[na:1.8.0_171]
at java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]
3. WebAsyncTask
callable 的方式,非常直觀簡單,但是我們經常關注的超時+異常的處理卻不太好,這個時候我們可以用WebAsyncTask
,實現姿勢也很簡單,包裝一下callable
,然後設定各種回撥事件即可
@RestController
@RequestMapping(path = "task")
public class WebAysncTaskRest {
@GetMapping(path = "get")
public WebAsyncTask<String> get(long sleep, boolean error) {
Callable<String> callable = () -> {
System.out.println("do some thing");
Thread.sleep(sleep);
if (error) {
System.out.println("出現異常,返回!!!");
throw new RuntimeException("異常了!!!");
}
return "hello world";
};
// 指定3s的超時
WebAsyncTask<String> webTask = new WebAsyncTask<>(3000, callable);
webTask.onCompletion(() -> System.out.println("over!!!"));
webTask.onTimeout(() -> {
System.out.println("超時了");
return "超時返回!!!";
});
webTask.onError(() -> {
System.out.println("出現異常了!!!");
return "異常返回";
});
return webTask;
}
}
4. DeferredResult
DeferredResult
與WebAsyncTask
最大的區別就是前者不確定什麼時候會返回結果,
DeferredResult
的這個特點,可以用來做實現很多有意思的東西,如後面將介紹的SseEmitter
就用到了它
下面給出一個例項
@RestController
@RequestMapping(path = "defer")
public class DeferredResultRest {
private Map<String, DeferredResult> cache = new ConcurrentHashMap<>();
@GetMapping(path = "get")
public DeferredResult<String> get(String id) {
DeferredResult<String> res = new DeferredResult<>();
cache.put(id, res);
res.onCompletion(new Runnable() {
@Override
public void run() {
System.out.println("over!");
}
});
return res;
}
@GetMapping(path = "pub")
public String publish(String id, String content) {
DeferredResult<String> res = cache.get(id);
if (res == null) {
return "no consumer!";
}
res.setResult(content);
return "over!";
}
}
在上面的例項中,使用者如果先訪問http://localhost:8080/defer/get?id=yihuihui
,不會立馬有結果,直到使用者再次訪問http://localhost:8080/defer/pub?id=yihuihui&content=哈哈
時,前面的請求才會有結果返回
那麼這個可以設定超時麼,如果一直把前端掛住,貌似也不太合適吧
- 在構造方法中指定超時時間:
new DeferredResult<>(3000L)
- 設定全域性的預設超時時間
@Configuration
@EnableWebMvc
public class WebConf implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 超時時間設定為60s
configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10));
}
}
II. 其他
0. 專案
相關博文
- 007-最佳化 web 請求三-非同步呼叫【WebAsyncTask】
- 高效能關鍵技術之---體驗 Spring MVC 的非同步模式(Callable、WebAsyncTask、DeferredResult) 基礎使用篇
系列博文
- 200105-SpringBoot 系列 web 篇之自定義返回 Http-Code 的 n 種姿勢
- 191222-SpringBoot 系列教程 web 篇之自定義請求匹配條件 RequestCondition
- 191206-SpringBoot 系列教程 web 篇 Listener 四種註冊姿勢
- 191122-SpringBoot 系列教程 web 篇 Servlet 註冊的四種姿勢
- 191120-SpringBoot 系列教程 Web 篇之開啟 GZIP 資料壓縮
- 191018-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南擴充套件篇
- 191016-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南
- 191012-SpringBoot 系列教程 web 篇之自定義異常處理 HandlerExceptionResolver
- 191010-SpringBoot 系列教程 web 篇之全域性異常處理
- 190930-SpringBoot 系列教程 web 篇之 404、500 異常頁面配置
- 190929-SpringBoot 系列教程 web 篇之重定向
- 190913-SpringBoot 系列教程 web 篇之返回文字、網頁、圖片的操作姿勢
- 190905-SpringBoot 系列教程 web 篇之中文亂碼問題解決
- 190831-SpringBoot 系列教程 web 篇之如何自定義引數解析器
- 190828-SpringBoot 系列教程 web 篇之 Post 請求引數解析姿勢彙總
- 190824-SpringBoot 系列教程 web 篇之 Get 請求引數解析姿勢彙總
- 190822-SpringBoot 系列教程 web 篇之 Beetl 環境搭建
- 190820-SpringBoot 系列教程 web 篇之 Thymeleaf 環境搭建
- 190816-SpringBoot 系列教程 web 篇之 Freemaker 環境搭建
- 190421-SpringBoot 高階篇 WEB 之 websocket 的使用說明
- 190327-Spring-RestTemplate 之 urlencode 引數解析異常全程分析
- 190317-Spring MVC 之基於 java config 無 xml 配置的 web 應用構建
- 190316-Spring MVC 之基於 xml 配置的 web 應用構建
- 190213-SpringBoot 檔案上傳異常之提示 The temporary upload location xxx is not valid
原始碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 專案原始碼: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/219-web-asyn
1. 一灰灰 Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人部落格 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題部落格 http://spring.hhui.top