![]()
去年有個做支付系統的讀者私信我,說他們搞了個"優雅降級"的方案,結果超時后用戶看到的是半成品頁面——商品價格加載了,庫存顯示"加載中",下單按鈕卻灰了。用戶瘋狂點擊,訂單系統直接被打穿。我問他怎么設計的,他說:"就加了個timeout啊。"
這就是典型的把超時當開關用。Java 21的預覽版結構化并發(Structured Concurrency,JEP 453)給了新工具,但工具不會替你回答那個關鍵問題:超時之后,系統該硬失敗,還是該交卷子上寫多少算多少?
這兩個模式的選擇,直接決定你的系統是"脆斷"還是"韌性"。
模式一:全失敗——要么全對,要么別答
先看最保守的打法。代碼很短,但藏著個關鍵假設:
整個響應必須完整,缺一塊就等于全錯。
這種場景在金融、醫療、核心交易里常見。你查個賬戶余額,如果風控評分沒算完,寧可告訴用戶"系統繁忙",也不能給個"大概也許可能"的數字。
實現起來用ShutdownOnFailure配合手動檢查截止時間:
```java public T runInScopeWithTimeout(Callable task, Duration timeout) throws Exception { Instant deadline = Instant.now().plus(timeout); try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var future = scope.fork(task); scope.join(); if (Instant.now().isAfter(deadline)) { throw new TimeoutException("Operation exceeded timeout: " + timeout); } scope.throwIfFailed(); return future.get(); } } ```
注意這里的順序:scope.join()先等,然后手動判超時,最后throwIfFailed()。有人可能會想,為啥不用joinUntil(deadline)?因為joinUntil到點拋異常,但你還沒檢查任務本身有沒有失敗。這個寫法保證了"超時"和"失敗"兩個維度都被覆蓋。
代價也很明顯。任何一個子任務慢半拍,整個請求陪葬。適合那種"錯一點就全錯"的業務,不適合用戶能容忍部分信息的場景。
模式二:部分結果——交卷時間到,寫多少算多少
再看另一種思路。假設你在拼一個推薦頁:用戶畫像、熱門商品、實時庫存,三個來源。畫像算出來了,熱門商品拿到了,庫存還在轉圈——這時候給用戶看前兩樣,比白屏強。
這就是joinUntil的用武之地。它不會幫你決定策略,但會把"時間到"這個信號暴露給你:
```java public List> executeWithPartialResults( List> tasks, Duration timeout) throws InterruptedException { Instant deadline = Instant.now().plus(timeout); try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { List> subtasks = new ArrayList<>(); for (Callable task : tasks) { subtasks.add(scope.fork(task)); } try { scope.joinUntil(deadline); } catch (TimeoutException ignored) { // 到點了,看看誰跑完了 } List> results = new ArrayList<>(subtasks.size()); for (var subtask : subtasks) { if (subtask.state() == StructuredTaskScope.Subtask.State.SUCCESS) { results.add(Optional.of(subtask.get())); } else { results.add(Optional.empty()); } } scope.shutdown(); return results; } } ```
這里的關鍵是subtask.state()。超時后,每個子任務的狀態可能是SUCCESS、FAILED、UNAVAILABLE——你得自己遍歷,自己決定怎么組裝響應。
有個細節容易踩坑:scope.shutdown()放在最后,是為了確保沒跑完的任務被中斷。但中斷不等于立刻停,那些任務可能還在后臺掙扎幾秒。如果你的下游服務沒做冪等,這可能會引發重復調用。
API不會替你做的三件事
結構化并發把子任務的生命周期管起來了,但設計決策全扔回給你。用這兩個模式之前,你得自己填三個空:
第一,超時的定義是什么?是從收到請求開始算,還是從第一個子任務啟動開始算?上面的代碼用的是前者,但有些場景需要后者——比如你要等線程池排隊。
第二,部分結果的排序怎么保證?返回的List>和輸入的List>順序一致,但用戶看到的界面可能需要按優先級重排。這個映射關系得自己維護。
第三,失敗的任務要不要重試?ShutdownOnFailure遇到第一個異常就觸發關閉,但超時場景下你可能想"能救一個是一個"。這時候得換ShutdownOnSuccess或者自己捕獲異常做降級。
原文作者提到一點我覺得很準:「joinUntil(...) only tells you the deadline was reached. It does not decide your response policy for you.」翻譯過來就是,API只管打鈴交卷,不管你怎么判分。
從Java 21到Java 25的坑
如果你現在就想在生產環境試,得先知道這串代碼是預覽版API。JEP 453在Java 21是預覽,后續版本里StructuredTaskScope的API形狀變了——構造方式、方法名、甚至包路徑都有調整。
原文作者給了一條遷移指引:看Part 9的Java 21到Java 25遷移指南。我沒找到那篇原文,但經驗是預覽版API的兼容層通常不會自動處理,升級JDK版本時得留人專門掃一遍并發相關的代碼。
編譯和運行記得加--enable-preview,CI/CD流水線里如果忘了這參數,編譯能通過但運行時會拋UnsupportedClassVersionError,排查起來很煩。
一個來自真實系統的反饋
去年某電商大促,有個團隊把商品詳情頁從串行改成結構化并行的部分結果模式。超時設了200ms,三個數據源:主站商品信息、第三方庫存、用戶個性化推薦。上線后發現,推薦服務P99延遲飆到300ms,但頁面沒崩——推薦那塊顯示"為你推薦加載中",用戶照樣能看價格加購物車。
他們的監控數據很有意思:超時觸發率12%,但用戶投訴率反而比全失敗模式低了40%。產品經理后來提了個需求,能不能讓"加載中"的區塊自動刷新?工程師說,那是另一個故事了。
你的系統里,有哪些接口是"寧可報錯也不能給半成品",哪些是"給多少算多少"?如果讓你現在重選,哪些會換策略?
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.