在上一篇中,我們大致介紹了響應式設計的原理及其應用情境。接下來,將進一步說明Reactive(非阻塞)與Blocking(阻塞)操作在實際應用中的區別。根據我個人的經驗,響應式程式設計的學習曲線較為陡峭,尤其是在開發前端應用程式和設備控制時,經常使用ReactiveX函式庫來處理異步資料流。不過,Quarkus並非直接基於RxJava,而是採用了Vert.x這個非同步處理架構。接下來,我會簡單介紹Vert.x的運作方式,這一點非常重要,因為它正是Quarkus在響應式架構中表現出色的關鍵之一(???)。
不過在聊Vert.x架構前,先提一下Servlet**。傳統的REST服務,**大多數基於Java EE(Java企業版)的服務使用Servlet
來實作。Servlet
是Java處理HTTP請求的一個標準,為同步機制(3.0以前),通常會佔用一個執行緒來處理每一個HTTP請求。下圖為一個典型的Java Servlet架構,展示了Web瀏覽器如何與Web伺服器和Servlet容器進行互動,進而連接到資料庫。
可以看到Servlet容器部分,負責管理多個Servlet。當收到請求時,Servlet容器會選擇合適的Servlet來處理該請求。它的細部流程如下
Step1 : Servlet
接收到請求,並將其綁定到伺服器的一個執行緒來進行處理。
Step2 : Servlet
可能會進行一些業務邏輯的初步處理,比如檢查請求的合法性或解析參數。
Step3 : 這部分通常是最耗時的。圖中的業務處理可能涉及到資料庫操作、外部API調用等耗時的操作。因為它是同步的,在業務處理完成之前,這個請求的執行緒會一直被佔用,無法釋放。這就是同步模型的一個特徵——請求必須等到業務邏輯完成,執行緒才能進入下一步。
Step4 : 線程結束:一旦業務處理完成,Servlet
會將結果返回給客戶端,並在這時結束這個執行緒。此時伺服器可以重新分配這個執行緒去處理新的請求。
不過在3.0後,Servlet
就加入非同步機制。另外你可以認知到,Servlet
其實就是Java EE 架構中用於處理 HTTP 請求的底層 API。稍微簡單戴一下整體跟JVM關係,當你啟動一個Java Web服務,啟動流程會是
角色運作職責為
所以當啟動一個 Java Web 服務時,通常是啟動了一個包含 Servlet 容器的應用程式伺服器(如 Tomcat)。這個伺服器運行在 JVM 中。
Servlet 講完了後,接下來來講Vert.x。Vert.x 是一個框架,它提供了一整套工具和函式庫,基於非阻塞、事件驅動的架構用來開發響應式、分散式和事件驅動的應用程式。且HTTP 處理不依賴 Servlet,它有自己的 HTTP 伺服器實作,完全獨立於 Servlet 規範。Vert.x 的 HTTP 處理基於非阻塞 I/O 和事件驅動模型,這與傳統的 Servlet 模型有很大不同。使用 Netty(一個非同步事件驅動的網路應用框架)作為底層,提供高效能的 HTTP 服務。
一樣借用網路現有架構圖如下,可以看到,Vert.x 的非阻塞事件驅動架構。
Host 與 JVM : 每個 JVM 都可以運行一個或多個 Vert.x Instance,每個Instance內部可以包含多個 Verticle。Verticle 是 Vert.x 中的基本單元,類似於 Servlet 這樣的 Web 組件,但與 Servlet 不同的是,Verticle 完全基於非阻塞的事件模型。
Verticle : 小型的應用邏輯單元,負責處理特定的業務邏輯或 I/O 操作。Vert.x Instancee可以管理和調度多個 Verticle 並發運行,而不會阻塞執行緒。
Event Bus : Vert.x 架構的核心組件之一。它允許 Verticle 之間相互通訊,無論它們是在同一個 JVM 內,還是分佈在不同的 JVM 中。請求和事件會在 Event Bus 中進行傳遞,Verticle 可以根據不同的事件進行處理,類似於消息隊列系統。這種設計方式非常適合高併發、分散式系統。
Event Handler(事件處理器):事件經由 Event Bus 傳遞後,由相應的 Event Handler 來處理。這些事件處理器可以處理各種 I/O 操作、業務邏輯或網路請求。
可以看到這樣的設計,如同前一章節提到的,架構符合Reactive 程式的核心特徵:非阻塞、事件驅動、高效能和可伸縮性。
這邊稍微提一下Netty,其實講到這邊,也許會思考Java WebFlux它底層的機制是什麼,其實也是Netty。Netty 是一個基於事件驅動的網路應用框架。下圖為Netty的框架圖
架構會有三大部分
Zero-Copy-Capable Rich Byte Buffer: Netty 高效處理 I/O 操作的基礎,Zero Copy允許在傳輸資料時,不需要多次在用戶態和核心態之間進行拷貝操作,這樣可大幅降低 CPU 和記憶體的消耗。說透了就是用高效的 Byte Buffer 來管理 I/O 資料,這讓網路 I/O 操作變得更加快速。
niversal Communication API : 提供了一個統一的通信 API,無論你是處理 HTTP、WebSocket 還是其他協定,Netty 都能以一致的方式來進行處理。這種抽象層的設計使得開發者能夠用相同的程式碼處理不同的協定,極大簡化了開發的複雜度。
Extensible Event Model : 事件驅動的框架,可擴展事件模型允許開發者自定義處理不同的 I/O 事件。
Netty 的 核心層 提供高效的 I/O 處理能力,傳輸服務層 支持多種傳輸方式,而 協定支持層 則讓 Netty 能夠處理多樣化的網路協定,這些層次相輔相成,使得Vert.x 可以藉由 Netty 高效運行並處理大量並發請求。
硬核的觀念講完後,接著來簡易提一下響應式寫法的差異性,假如現在有一個需求會有兩個資料原,並且要將兩個資料原彙整後再回傳,以下分別用阻塞和非阻塞的寫法來說明。
阻塞寫法通常是線性、同步的,這代表執行緒在等待I/O操作完成時會被「阻塞」。例如,假設有兩個資料源Data1
和Data2
,需要彙整兩者後返回結果:
public Data aggregateData() {
Data data1 = fetchDataFromDatabase(); // 請求資料庫,此時執行緒被阻塞
Data data2 = fetchDataFromExternalAPI(); // 請求外部 API,此時執行緒再次被阻塞
return combine(data1, data2); // 整合兩部分的資料
}
fetchDataFromDatabase()
完成後,才會發出下一個請求fetchDataFromExternalAPI()
。這樣的寫法導致無法充分利用系統的資源,因為每個I/O操作都需要等待結果,從而增加了處理時間。
響應式程式設計使用非阻塞I/O,允許在等待操作完成的同時繼續執行其他任務。這樣可以同時處理多個操作,大幅提高效能。我們使用Mutiny框架來完成:
public Uni<Data> aggregateData() {
Uni<Data> data1Uni = fetchDataFromDatabaseAsync(); // 非阻塞地請求資料庫
Uni<Data> data2Uni = fetchDataFromExternalAPIAsync(); // 非阻塞地請求外部 API
return Uni.combine().all().unis(data1Uni, data2Uni) // 同時等待兩個 Uni 完成
.asTuple()
.onItem().transform(tuple -> combine(tuple.getItem1(), tuple.getItem2()));
}
利用Uni
類型來代表異步操作。data1Uni
和data2Uni
會同時發送請求,無需等待前一個操作完成,減少了等待時間,從而提升了程序的處理效能。
Uni和Multi是Mutiny框架中處理異步任務的核心抽象
Uni<?>:代表0或1個結果的異步操作。類似於Java中的CompletionStage
,通常用來表示單一結果的操作。
// 建立一個直接回傳值的Uni
Uni<String> uniFromValue = Uni.createFrom().item("Hello, Mutiny!");
// 創建一個有邏輯處理過後的Uni
Uni<String> uniFromSupplier = Uni.createFrom().item(() -> {
// 進行某些操作
return "Async result";
});
可以通過以下方式將CompletionStage
轉換為Uni
:
Uni<String> uniFromCompletionStage = Uni.createFrom().completionStage(this::asyncMethod);
Multi<?>:表示多個結果的異步操作,適用於處理多筆資料或事件流的情境。
Multi<String> names = Multi.createFrom().items("Alice", "Bob", "Charlie");
Multi<Integer> lengths = names
.filter(name -> !name.equals("Bob")) // 過濾掉 "Bob"
.onItem().transform(String::length); // 將名稱轉換為其長度
在處理非同步操作時,錯誤管理是一個重要部分。Mutiny提供多種錯誤處理機制,允許在操作失敗時進行補救。例如:
Uni<String> recovered = uniFromValue
.onFailure()
.recoverWithItem("Fallback item in case of failure");
儘管Uni
是非阻塞的,也可以將其轉換為阻塞的同步操作,但需注意這會失去響應式程式設計的效能優勢。例如
String result = uniFromValue.await().indefinitely();
System.out.println(result); // 輸出:Hello, Mutiny!
從多個API來源同時獲取資料,然後在所有資料到達後進行彙整時,範例如下,同時發送兩個請求,並使用combine
(Mutiny中的Uni.combine()
)來等待兩個非阻塞操作完成,再進行資料合併。
public Uni<Data> aggregateData() {
Uni<Data> data1Uni = fetchDataFromDatabaseAsync(); // 非阻塞地請求資料庫
Uni<Data> data2Uni = fetchDataFromExternalAPIAsync(); // 非阻塞地請求外部 API
return Uni.combine().all().unis(data1Uni, data2Uni) // 同時等待兩個 Uni 完成
.asTuple()
.onItem().transform(tuple -> combine(tuple.getItem1(), tuple.getItem2()));
}
等某個操作完成後,再啟動另一個異步操作。例如,當需要先查詢一個API,然後根據它的結果去查詢另一個API時
public Uni<Data> fetchAndProcessData() {
return fetchDataFromDatabaseAsync() // 第一步,取得資料庫資料
.onItem().transformToUni(data1 -> fetchFromApiAsync(data1)) // 第二步,依據data1結果進行API查詢
.onItem().transform(apiData -> process(apiData)); // 第三步,處理API結果
}
一些更複雜的情境下,可能會遇到需要同時發送多個請求,但某些請求需要根據其他請求的結果來決定下一步。這種「並行與串聯混合」的需求可以通過結合combine
和flatMap
來實現。例如,假設你有三個API,API1
和API2
需要同時查詢,API3
的查詢取決於API1
的結果。
public Uni<Data> processMultipleApis() {
Uni<Data1> api1 = fetchFromApi1Async(); // 非阻塞請求 API1
Uni<Data2> api2 = fetchFromApi2Async(); // 非阻塞請求 API2
return Uni.combine().all().unis(api1, api2) // 並行執行 API1 和 API2
.asTuple()
.onItem().transformToUni(tuple -> fetchFromApi3Async(tuple.getItem1())) // API1完成後執行 API3
.onItem().transform(api3Data -> combineResults(api3Data, tuple.getItem2())); // 最後合併API2和API3的結果
}
在分布式系統中,網絡請求容易失敗,因此錯誤處理和回退(fallback)策略非常重要。
如onFailure()
(Mutiny),應對異常情況。
public Uni<Data> fetchWithFallback() {
return fetchFromApiAsync()
.onFailure().recoverWithItem(() -> getCachedData()); // 請求失敗時使用本地快取
}
重試也是常見的錯誤處理策略之一,範例如下
public Uni<Data> fetchWithRetry() {
return fetchFromApiAsync()
.onFailure().retry().atMost(3); // 失敗時重試最多3次
}
不過稍微提一下,響應式設計面臨的一個挑戰是,許多傳統的後端套件或依賴仍然是同步的,這時就需要我們手動將它們「包裝」成非阻塞的操作,以融入響應式工作流。例如,資料庫操作、文件系統讀寫或某些外部API呼叫,可能仍然是阻塞式API。例如
Uni<String> uniFromCompletionStage = Uni.createFrom().completionStage(() -> someBlockingMethod());
響應式程式設計在後端的場景經常用於解決高併發、多API聚合、串聯邏輯處理以及實時資料流的問題。這些情境下,響應式框架的並行處理與異步資料流能力能顯著提升系統效能。