概覽
為什麼建立 Spring WebFlux?
部分原因是需要一個非阻塞的 Web 棧,以使用少量執行緒處理併發並以更少的硬體資源進行擴充套件。Servlet 非阻塞 I/O 使得其脫離了 Servlet API 的其餘部分,其中契約是同步的(Filter、Servlet)或阻塞的(getParameter、getPart)。這是建立新的通用 API 的動機,該 API 可作為任何非阻塞執行時的基礎。這很重要,因為像 Netty 這樣的伺服器在非同步、非阻塞領域已經非常成熟。
答案的另一部分是函數語言程式設計。就像 Java 5 中新增的註解創造了機會(例如帶註解的 REST 控制器或單元測試)一樣,Java 8 中新增的 Lambda 表示式也為 Java 中的函式式 API 創造了機會。這對非阻塞應用程式和延續式 API(透過 CompletableFuture 和 ReactiveX 流行起來)來說是一個福音,它們允許宣告性地組合非同步邏輯。在程式設計模型級別,Java 8 使 Spring WebFlux 能夠提供函式式 Web 端點以及帶註解的控制器。
定義“響應式”
我們提到了“非阻塞”和“函式式”,但“響應式”到底是什麼意思?
術語“響應式”指的是圍繞對變化做出反應而構建的程式設計模型——網路元件對 I/O 事件做出反應,UI 控制器對滑鼠事件做出反應,等等。從這個意義上說,非阻塞是響應式的,因為我們不再被阻塞,而是以對操作完成或資料可用時的通知做出反應的模式。
我們 Spring 團隊還將另一個重要的機制與“響應式”聯絡起來,那就是非阻塞背壓。在同步的命令式程式碼中,阻塞呼叫作為一種自然的背壓形式,強制呼叫者等待。在非阻塞程式碼中,控制事件速率變得很重要,以防止快速生產者壓倒其目的地。
響應式流(Reactive Streams)是一個小規範(也在 Java 9 中採用),它定義了帶有背壓的非同步元件之間的互動。例如,資料倉庫(作為釋出者)可以生成資料,然後 HTTP 伺服器(作為訂閱者)可以將資料寫入響應。響應式流的主要目的是讓訂閱者控制釋出者生成資料的速度。
|
常見問題:如果釋出者無法減速怎麼辦? 響應式流的目的只是建立機制和邊界。如果釋出者無法減速,它必須決定是緩衝、丟棄還是失敗。 |
響應式 API
響應式流對於互操作性起著重要作用。它對庫和基礎設施元件很有用,但作為應用程式 API 則不太有用,因為它太底層了。應用程式需要更高級別、更豐富的函式式 API 來組合非同步邏輯——類似於 Java 8 的 Stream API,但不僅限於集合。這就是響應式庫所扮演的角色。
Reactor 是 Spring WebFlux 首選的響應式庫。它提供了 Mono 和 Flux API 型別,透過與 ReactiveX 運算子詞彙表對齊的豐富運算子集來處理 0..1 (Mono) 和 0..N (Flux) 的資料序列。Reactor 是一個響應式流庫,因此,它的所有運算子都支援非阻塞背壓。Reactor 非常關注伺服器端 Java。它與 Spring 緊密合作開發。
WebFlux 需要 Reactor 作為核心依賴,但它透過響應式流與其他響應式庫互操作。通常,WebFlux API 接受純 Publisher 作為輸入,將其內部適配到 Reactor 型別,使用它,並返回 Flux 或 Mono 作為輸出。因此,您可以將任何 Publisher 作為輸入傳遞,並且可以對輸出應用操作,但您需要將輸出適配以用於另一個響應式庫。只要可行(例如,帶註解的控制器),WebFlux 就會透明地適配使用 RxJava 或另一個響應式庫。有關更多詳細資訊,請參閱響應式庫。
| 除了響應式 API,WebFlux 還可以與 Kotlin 中的協程 API 一起使用,它提供了一種更命令式的程式設計風格。以下 Kotlin 程式碼示例將與協程 API 一起提供。 |
程式設計模型
spring-web 模組包含 Spring WebFlux 所依賴的響應式基礎,包括 HTTP 抽象、支援伺服器的響應式流介面卡、編解碼器以及核心WebHandler API,它可與 Servlet API 媲美,但具有非阻塞契約。
在此基礎上,Spring WebFlux 提供了兩種程式設計模型選擇:
適用性
Spring MVC 還是 WebFlux?
一個很自然的問題,但它建立了一個不健全的二分法。實際上,兩者協同工作以擴充套件可用選項的範圍。兩者都旨在實現連續性和一致性,它們並存,並且來自每一方的反饋都使雙方受益。下圖顯示了兩者之間的關係,它們的共同點以及各自獨特支援的內容:
我們建議您考慮以下具體要點:
-
如果您有一個執行良好的 Spring MVC 應用程式,則無需更改。指令式程式設計是編寫、理解和除錯程式碼的最簡單方法。由於歷史上大多數庫都是阻塞的,因此您可以最大程度地選擇庫。
-
如果您已經在尋找非阻塞 Web 棧,Spring WebFlux 提供了與該領域中其他棧相同的執行模型優勢,還提供了伺服器選擇(Netty、Tomcat、Jetty 和 Servlet 容器)、程式設計模型選擇(註解控制器和函式式 Web 端點)以及響應式庫選擇(Reactor、RxJava 或其他)。
-
如果您對用於 Java 8 Lambda 或 Kotlin 的輕量級函式式 Web 框架感興趣,可以使用 Spring WebFlux 函式式 Web 端點。對於需求不那麼複雜的小型應用程式或微服務,這也可以是一個不錯的選擇,它們可以受益於更高的透明度和控制。
-
在微服務架構中,您可以混合使用 Spring MVC 或 Spring WebFlux 控制器,或者使用 Spring WebFlux 函式式端點的應用程式。在兩個框架中都支援相同的基於註解的程式設計模型,這使得重用知識變得更容易,同時也能為正確的工作選擇正確的工具。
-
評估應用程式的一種簡單方法是檢查其依賴項。如果您有阻塞的持久化 API(JPA、JDBC)或網路 API 要使用,至少對於常見架構而言,Spring MVC 是最佳選擇。使用 Reactor 和 RxJava 在單獨的執行緒上執行阻塞呼叫在技術上是可行的,但您將無法充分利用非阻塞 Web 棧。
-
如果您有一個呼叫遠端服務的 Spring MVC 應用程式,請嘗試響應式
WebClient。您可以直接從 Spring MVC 控制器方法返回響應式型別(Reactor、RxJava 或其他)。每次呼叫的延遲越大,或者呼叫之間的相互依賴性越大,收益就越顯著。Spring MVC 控制器也可以呼叫其他響應式元件。 -
如果您的團隊規模龐大,請記住轉向非阻塞、函式式和宣告式程式設計的學習曲線很陡峭。一種無需完全轉換即可開始的實用方法是使用響應式
WebClient。除此之外,從小處著手並衡量收益。我們預計,對於廣泛的應用程式而言,這種轉換是不必要的。如果您不確定要尋找哪些收益,請從瞭解非阻塞 I/O 的工作原理(例如,單執行緒 Node.js 上的併發)及其影響開始。
伺服器
Spring WebFlux 支援 Tomcat、Jetty、Servlet 容器,以及 Netty 等非 Servlet 執行時。所有伺服器都適配到一個低階通用 API,以便跨伺服器支援高階程式設計模型。
Spring WebFlux 不內建支援啟動或停止伺服器。然而,從 Spring 配置和WebFlux 基礎設施組裝應用程式並用幾行程式碼執行它很容易。
Spring Boot 有一個 WebFlux 啟動器,可以自動化這些步驟。預設情況下,該啟動器使用 Netty,但透過更改 Maven 或 Gradle 依賴項可以輕鬆切換到 Tomcat 或 Jetty。Spring Boot 預設使用 Netty,因為它在非同步、非阻塞領域更廣泛使用,並且允許客戶端和伺服器共享資源。
Tomcat 和 Jetty 都可以與 Spring MVC 和 WebFlux 一起使用。但是,請記住,它們的使用方式非常不同。Spring MVC 依賴於 Servlet 阻塞 I/O,並允許應用程式在需要時直接使用 Servlet API。Spring WebFlux 依賴於 Servlet 非阻塞 I/O,並在低階介面卡後面使用 Servlet API。它不直接暴露給應用程式使用。
| 強烈建議在 WebFlux 應用程式的上下文中不要對映 Servlet 過濾器或直接操作 Servlet API。由於上述原因,在相同的上下文中混合阻塞 I/O 和非阻塞 I/O 將導致執行時問題。 |
效能
效能有許多特性和含義。響應式和非阻塞通常不會使應用程式執行得更快。在某些情況下可以——例如,如果使用 WebClient 並行執行遠端呼叫。然而,以非阻塞方式做事需要更多的工作,這可能會略微增加所需的處理時間。
響應式和非阻塞的關鍵預期好處是能夠以少量固定執行緒和更少的記憶體進行擴充套件。這使得應用程式在負載下更具彈性,因為它們以更可預測的方式進行擴充套件。然而,為了觀察這些好處,您需要有一些延遲(包括慢速和不可預測的網路 I/O 的混合)。這正是響應式棧開始展現其優勢的地方,並且差異可能非常顯著。
併發模型
Spring MVC 和 Spring WebFlux 都支援註解控制器,但在併發模型以及阻塞和執行緒的預設假設方面存在關鍵差異。
在 Spring MVC(以及一般的 Servlet 應用程式)中,假設應用程式可以阻塞當前執行緒(例如,用於遠端呼叫)。因此,Servlet 容器使用一個大型執行緒池來吸收請求處理期間可能發生的阻塞。
在 Spring WebFlux(以及一般的非阻塞伺服器)中,假設應用程式不會阻塞。因此,非阻塞伺服器使用一個小型、固定大小的執行緒池(事件迴圈工作執行緒)來處理請求。
| “擴充套件”和“少量執行緒”聽起來可能相互矛盾,但永遠不阻塞當前執行緒(而是依賴回撥)意味著您不需要額外的執行緒,因為沒有阻塞呼叫需要吸收。 |
呼叫阻塞 API
如果確實需要使用阻塞庫怎麼辦?Reactor 和 RxJava 都提供了 publishOn 運算子,可以在不同的執行緒上繼續處理。這意味著有一個簡單的退出機制。但是請記住,阻塞 API 與這種併發模型不相容。
可變狀態
在 Reactor 和 RxJava 中,您透過運算子宣告邏輯。在執行時,形成了一個響應式管道,其中資料按順序在不同的階段進行處理。這樣做的一個主要好處是,它使應用程式無需保護可變狀態,因為該管道中的應用程式程式碼永遠不會併發呼叫。
執行緒模型
在執行 Spring WebFlux 的伺服器上,您應該期望看到哪些執行緒?
-
在一個“純粹的”Spring WebFlux 伺服器上(例如,沒有資料訪問或其他可選依賴),您可以預期一個伺服器執行緒和幾個用於請求處理的執行緒(通常與 CPU 核數相同)。然而,Servlet 容器可能會啟動更多的執行緒(例如,Tomcat 上有 10 個),以支援 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 的使用。
-
響應式
WebClient以事件迴圈方式執行。因此,您可以看到與之相關的少量固定處理執行緒(例如,使用 Reactor Netty 聯結器時的reactor-http-nio-)。然而,如果 Reactor Netty 同時用於客戶端和伺服器,兩者預設共享事件迴圈資源。 -
Reactor 和 RxJava 提供了執行緒池抽象,稱為排程器,用於與
publishOn運算子一起使用,該運算子用於將處理切換到不同的執行緒池。排程器的名稱暗示了特定的併發策略——例如,“並行”(用於 CPU 密集型工作,執行緒數量有限)或“彈性”(用於 I/O 密集型工作,執行緒數量多)。如果您看到此類執行緒,則表示某些程式碼正在使用特定的執行緒池Scheduler策略。 -
資料訪問庫和其他第三方依賴項也可以建立和使用自己的執行緒。