1、背景
在項目開發中,有一個流程性的方法執行,這個方法會調用各種方法,可能會導致時間比較長 ,如果一直等待響應結果的話,可能會造成超時,如果直接使用異步的方式的話,前端無法知道整體流程什么時候會結束,
2、解決方案
使用了DeferredResult 的方式,設置超時時間,當流程執行完了沒有超過指定時間就可以直接返回結果,如果超過了指定時間就給前端先返回超時結果,并且指定一個唯一標志放到結果中返回,前端后續可以拿著這個唯一標志來輪詢,知道返回執行完成
關于 DeferredResult :請求的處理線程(即 tomcat 線程池的線程)不會等DeferredResult#setResult() 被調用才釋放,而是直接釋放了。
也就是說 tomcat 線程安排好 DeferredResult 的一些配置后,不會等邏輯處理完(DeferredResult->setResult()的調用或者超時)。
而是直接釋放了,這樣 tomcat 線程就被回收到線程池中了,可以響應其他請求,不會傻傻地阻塞等著 DeferredResult->setResult() 被調用或超時。
我們都知道 tomcat 的線程池大小是有限的,如果我們的一些業務邏輯處理慢的話,會漸漸地占滿 tomcat 線程,這樣就無法處理新的請求,所以一些處理緩慢的業務我們會放到業務線程池中處理,但單純的放到業務線程池中處理的話,我們無法得知其什么時候處理完,也無法將處理完的結果和之前的請求匹配上,所以常做的方式就是輪詢。
而 DeferredResult 的做法就類似僅把事情安排好,不會管事情做好沒,tomcat 線程就釋放走了,注意此時不會給請求方(如瀏覽器)任何響應,而是將請求存放在一邊,等后面有結果了再把之前的請求拿來,把值響應給請求方。
用簡單的話來總結下 Spring DeferredResult :如果返回值類型是 DeferredResult 則表明其是異步請求,tomcat 線程不會等到應用程序處理完或者超時,而是會立即釋放線程。
而這個未處理完的請求則會暫存,tomcat 知曉其為異步請求,也不會對客戶端進行響應,直至 tomcat 線程掃描到請求超時或者應用線程將 result 塞入到 DeferredResult 中。
3、一些常用方法
public void onTimeout(Runnable callback)
public void onError(Consumer<Throwable> callback)
public void onCompletion(Runnable callback)
public boolean setResult(T result)
onTimeout()
:僅超時觸發。onError()
:僅異步任務拋出異常時觸發。onCompletion()
是兜底回調,無論何種結束方式都會執行,適合釋放共享資源(如移除緩存、關閉連接)- setResult() 設置返回結果集
4、具體簡單代碼實現
// -1 表示任務未完成 0 表示任務失敗 其它是具體值private static final Map<String,String> EXEC_CACHE = new ConcurrentHashMap<>();@Overridepublic DeferredResult<Map<String, String>> exec() {DeferredResult<Map<String, String>> deferredResult = new DeferredResult<>(5000L);// 設置超時回調(如果任務未在 2 秒內完成,返回超時響應)String flag = UUID.randomUUID().toString();Map<String, String> result = new HashMap<>();deferredResult.onTimeout(() -> {result.put("code", "410");result.put("message", "任務未完成,已超時!");result.put("flag",flag);deferredResult.setResult(result);EXEC_CACHE.put(flag, "-1");});//執行具體任務CompletableFuture.runAsync(()->{try {//模擬執行耗時Thread.sleep(1000);//模擬獲到執行結果String execResult = UUID.randomUUID().toString();//放入緩存EXEC_CACHE.put(flag, execResult);//完成響應result.put("code", "200");result.put("message", "任務完成");result.put("flag",execResult);deferredResult.setResult(result);}catch (Exception e){//完成響應result.put("code", "500");result.put("message", "任務完成");result.put("flag",null);deferredResult.setResult(result);EXEC_CACHE.put(flag, "0");}});return deferredResult;}@Overridepublic String queryExecResult(String flag) {//前端根據具體值判斷要不要繼續輪詢return EXEC_CACHE.get(flag);}
執行超時情況
輪詢查詢
未超時返回