反應式程式設計筆記 (第一部分):反應式概觀

工程 | Dave Syer | 2016 年 6 月 7 日 | ...

反應式程式設計 (Reactive Programming) 再次引起人們的興趣,而且目前有很多相關的雜音,對於像作者這樣的外行和簡單的企業 Java 開發人員來說,並不是所有內容都非常容易理解。 本文(本系列的第一篇)可能有助於澄清您對此問題的理解。 這種方法盡可能具體,並且沒有提及「指稱語義 (denotational semantics)」。 如果您正在尋找更學術的方法和大量 Haskell 中的程式碼範例,網路上有很多,但您可能不想在這裡。

反應式程式設計經常與並行程式設計和高效能混為一談,以至於很難將這些概念分開,但實際上它們原則上是完全不同的。 這不可避免地導致混淆。 反應式程式設計也經常被稱為或與函數式反應式程式設計 (Functional Reactive Programming) 或 FRP 混為一談(我們在這裡互換使用這兩個術語)。 有些人認為 Reactive 並沒有什麼新鮮的,而且他們每天都在做(主要是使用 JavaScript)。 其他人似乎認為這是 Microsoft 的一份禮物(他們在發布一些 C# 擴充功能時大肆宣傳)。 在企業 Java 領域,最近出現了一些熱潮(例如,請參閱 Reactive Streams 計畫),並且與任何閃亮的新事物一樣,在外頭有很多關於何時、何地可以以及應該使用它的錯誤。

它是什麼?

反應式程式設計是一種微架構風格,涉及事件的智慧型路由和消費,所有這些結合起來改變行為。 這有點抽象,您在網路上遇到的許多其他定義也是如此。 我們嘗試建立一些更具體的概念,即什麼是反應式,或者為什麼它在接下來的內容中可能很重要。

反應式程式設計的起源可能可以追溯到 1970 年代甚至更早,因此這個想法沒有什麼新鮮的,但它們確實與現代企業中的某些東西產生共鳴。 這種共鳴(並非偶然地)與微服務的興起以及多核心處理器的普及同時出現。 其中一些原因希望能夠變得清楚。

以下是來自其他來源的一些有用的簡要定義

反應式程式設計背後的基本概念是,存在某些資料類型,它們表示「隨著時間」的值。 涉及這些隨時間變化的值的計算本身也將具有隨時間變化的值。

還有…

了解它的一個簡單方法是想像您的程式是一個電子表格,並且您的所有變數都是儲存格。 如果電子表格中的任何儲存格發生變化,則引用該儲存格的任何儲存格也會發生變化。 FRP 也是如此。 現在想像一下,有些儲存格會自行改變(或者更確切地說,是從外部世界獲取的):在 GUI 情況下,滑鼠的位置將是一個很好的例子。

(來自 Stackoverflow 上的術語問題

FRP 與高效能、並行、非同步操作和非阻塞 IO 具有很強的親和力。 然而,從懷疑 FRP 與其中任何一個都無關開始可能是有幫助的。 當然,當使用反應式模型時,這些問題可以自然地處理,通常對呼叫者是透明的。 但是,在有效或高效地處理這些問題方面,實際的好處完全取決於相關的實作(因此應該受到高度審查)。 也可以以同步、單線程的方式實作一個完全健全且有用的 FRP 框架,但這對於嘗試使用任何新工具和函式庫來說,實際上不太可能有幫助。

反應式使用案例

對於新手來說,最難回答的問題似乎是「它有什麼好處?」。 以下是一些來自企業環境的範例,說明了一般的使用模式

外部服務呼叫 現在許多後端服務都是 REST 風格的(即,它們透過 HTTP 運作),因此底層協議從根本上是阻塞和同步的。 也許這不是 FRP 的明顯領域,但實際上它是一個非常肥沃的領域,因為這種服務的實作通常涉及呼叫其他服務,然後根據第一次呼叫的結果呼叫更多服務。 如果有這麼多的 IO 發生,如果您在傳送下一個請求之前等待一個呼叫完成,那麼您可憐的客戶端會在您設法組裝回覆之前放棄。 因此,外部服務呼叫,特別是呼叫之間複雜的依賴關係協調,是優化的好東西。 FRP 提供了驅動這些操作的邏輯的「可組合性」承諾,以便更容易為呼叫服務的開發人員編寫程式碼。

高度並行的訊息消費者 訊息處理,特別是在高度並行時,是一種常見的企業使用案例。 像這樣的反應式框架喜歡測量微基準,並吹噓您可以在 JVM 中每秒處理多少訊息。 結果確實令人震驚(每秒可以輕鬆實現數千萬條訊息),但可能有點人為 - 如果他們說他們正在基準測試一個簡單的「for」迴圈,您就不會那麼印象深刻。 然而,我們不應該急於放棄這項工作,並且很容易看到,當效能至關重要時,應該欣然接受所有貢獻。 反應式模式自然地適用於訊息處理(因為事件可以很好地轉換為訊息),因此如果有一種方法可以更快地處理更多訊息,我們應該注意。

電子表格 也許不是真正的企業使用案例,但企業中的每個人都可以輕鬆地與之相關,並且它很好地捕捉了 FRP 的哲學和實作難度。 如果儲存格 B 依賴於儲存格 A,並且儲存格 C 同時依賴於儲存格 A 和 B,那麼您如何傳播 A 中的變更,確保在將任何變更事件傳送到 B 之前更新 C? 如果您有一個真正的反應式框架可以構建,那麼答案是「您不在乎,您只需宣告依賴關係」,這確實是電子表格的核心力量。 它還突出了 FRP 和簡單的事件驅動程式設計之間的差異 - 它將「智慧型」放入「智慧型路由」中。

(非)同步處理的抽象 這更像是一個抽象的使用案例,因此偏離了我們可能應該避免的領域。 這與已經提到的更具體的使用案例之間也存在一些(很多)重疊,但希望它仍然值得討論。 基本主張是一個熟悉(且合理)的主張,即只要開發人員願意接受額外的抽象層,他們就可以忘記他們呼叫的程式碼是同步還是非同步。 由於處理非同步程式設計會耗費寶貴的腦細胞,因此可能有一些有用的想法。 反應式程式設計並不是解決這個問題的唯一方法,但 FRP 的一些實作人員已經對這個問題進行了足夠的思考,以至於他們的工具很有用。

這個 Netflix 部落格有一些關於現實生活使用案例的非常有用和具體的範例:Netflix 技術部落格:Netflix API 中使用 RxJava 實現的函數式反應式程式設計

比較

如果你自 1970 年以來就沒有住在山洞裡,那你一定會遇到一些與反應式程式設計相關的其他概念,以及人們試圖用它來解決的各種問題。以下是一些概念,以及我個人對它們相關性的看法

Ruby Event-Machine Event Machine 是對並行程式設計(通常涉及非阻塞 IO)的一種抽象。Rubyists 長期以來都在努力將一種設計用於單執行緒腳本的語言,轉變成可以用來編寫伺服器應用程式的語言,使其 a) 可以運作,b) 效能良好,以及 c) 在負載下保持存活。Ruby 已經有多執行緒相當長一段時間了,但它們的使用不多,並且聲譽不佳,因為它們的效能並不總是很好。另一種選擇,現在已經無處不在,因為它已經被提升(在 Ruby 1.9 中)到語言的核心,是 Fibers (原文如此)。Fiber 程式設計模型有點像是協程的一種變體(請參閱下文),其中使用單個原生執行緒來處理大量並發請求(通常涉及 IO)。程式設計模型本身有點抽象且難以理解,因此大多數人使用包裝器,而 Event Machine 是最常見的。Event Machine 不一定使用 Fibers(它抽象了這些問題),但很容易找到在 Ruby Web 應用程式中使用 Event Machine 和 Fibers 的程式碼範例(例如,請參閱 Ilya Grigorik 的這篇文章,或來自 em-http-request 的 fibered 範例)。人們經常這樣做,以獲得在 I/O 密集型應用程式中使用 Event Machine 所帶來的可擴展性的好處,而無需使用大量巢狀回呼所產生的醜陋程式設計模型。

Actor 模型 與物件導向程式設計類似,Actor 模型是電腦科學的一個深遠的脈絡,可以追溯到 1970 年代。Actor 提供了一種計算抽象(相對於資料和行為),可以將並發作為一種自然的結果,因此在實務上,它們可以構成並發系統的基礎。Actor 互相發送訊息,所以在某種意義上它們是反應式的,並且在自稱為 Actor 或 Reactive 的系統之間存在很多重疊。通常,區別在於它們的實現層面(例如,Akka 中的 Actors 可以分佈在不同的進程中,這是該框架的一個顯著特徵)。

延遲結果 (Futures) Java 1.5 引入了一組豐富的新函式庫,包括 Doug Lea 的 "java.util.concurrent",其中一部分是延遲結果的概念,封裝在 Future 中。這是在非同步模式上進行簡單抽象的一個很好的例子,而無需強制實現為非同步,或使用任何特定的非同步處理模型。正如 Netflix Tech Blog:在 Netflix API 中使用 RxJava 的函數式反應式程式設計 很好地展示的那樣,當你只需要並行處理一組相似的任務時,Futures 非常棒,但是一旦它們中的任何一個想要相互依賴或有條件地執行時,你就會陷入一種 "巢狀回呼地獄" 的形式。反應式程式設計提供了解毒劑。

Map-reduce 和 fork-join 對並行處理的抽象非常有用,並且有很多範例可供選擇。Map-reduce 和 fork-join 最近在 Java 世界中得到發展,受到大規模並行分散式處理(MapReduceHadoop)以及 JDK 本身在 1.7 版中的驅動(Fork-Join)。這些是有用的抽象,但(與延遲結果一樣)它們與 FRP 相比是淺薄的,FRP 可以用作簡單並行處理的抽象,但它可以超越該範圍進入可組合性和宣告式通訊。

協程 "協程" 是 "子程式" 的一種推廣 — 它有一個進入點和一個或多個離開點,就像一個子程式一樣,但是當它離開時,它將控制權傳遞給另一個協程(不一定傳遞給它的調用者),並且它累積的任何狀態都會被保留並記住以供下次調用。協程可以用作 Actor 和 Streams 等更高級別功能的構建模塊。反應式程式設計的目標之一是提供相同類型的通訊並行處理代理的抽象,因此協程(如果它們可用)是一個有用的構建模塊。有各種各樣的協程,其中一些比一般情況更具限制性,但比普通的子程式更靈活。Fibers(請參閱關於 Event Machine 的討論)是一種變體,而 Generators(在 Scala 和 Python 中很常見)是另一種變體。

Java 中的反應式程式設計

Java 不是一種 "反應式語言",因為它本身並不支援協程。JVM 上還有其他語言(Scala 和 Clojure)更原生支援反應式模型,但 Java 本身直到第 9 版才支援。然而,Java 是企業開發的動力源,並且最近在 JDK 之上提供反應式層方面有很多活動。我們在這裡只對其中的幾個進行非常簡要的介紹。

Reactive Streams 是一個非常低層級的合約,表示為少量的 Java 介面(加上 TCK),但也適用於其他語言。這些介面表示 PublisherSubscriber 的基本構建模塊,具有顯式的反向壓力,形成可互操作函式庫的通用語言。Reactive Streams 已作為 java.util.concurrent.Flow 納入 JDK 第 9 版。該專案是來自 Kaazing、Netflix、Pivotal、Red Hat、Twitter、Typesafe 和許多其他公司的工程師之間的合作。

RxJava:Netflix 一段時間以來一直在內部使用反應式模式,然後他們將他們正在使用的工具以開放原始碼許可證的形式發布為 Netflix/RxJava(隨後重新命名為 "ReactiveX/RxJava")。Netflix 在 RxJava 之上使用 Groovy 進行大量程式設計,但它對 Java 的使用是開放的,並且非常適合透過使用 Lambdas 的 Java 8。有一個 通往 Reactive Streams 的橋樑。根據 David Karnok 的 反應式世代分類,RxJava 是一個 "第二代" 函式庫。

Reactor 是一個來自 Pivotal 開源團隊(創建 Spring 的團隊)的 Java 框架。它直接建立在 Reactive Streams 之上,因此無需橋樑。Reactor IO 專案提供圍繞 Netty 和 Aeron 等低層級網路運行時的包裝器。根據 David Karnok 的 反應式世代分類,Reactor 是一個 "第四代" 函式庫。

Spring Framework 5.0(第一個里程碑是 2016 年 6 月)具有內建的反應式功能,包括用於構建 HTTP 伺服器和客戶端的工具。Web 層中 Spring 的現有使用者會發現一個非常熟悉的程式設計模型,使用註釋來裝飾控制器方法以處理 HTTP 請求,在很大程度上將反應式請求的調度和反向壓力問題移交給框架。Spring 建立在 Reactor 之上,但也公開了 API,允許使用多種函式庫(例如 Reactor 或 RxJava)來表達其功能。使用者可以從 Tomcat、Jetty、Netty(透過 Reactor IO)和 Undertow 中選擇伺服器端網路堆疊。

Ratpack 是一組用於透過 HTTP 構建高效能服務的函式庫。它建立在 Netty 之上,並實現了 Reactive Streams 以實現互操作性(因此你可以在堆疊中更高層次使用其他 Reactive Streams 實現,例如)。Spring 作為一個原生元件被支援,並且可以使用一些簡單的公用程式類來提供依賴注入。還有一些自動配置,以便 Spring Boot 使用者可以將 Ratpack 嵌入到 Spring 應用程式中,啟動一個 HTTP 端點並在那裡監聽,而不是使用 Spring Boot 直接提供的嵌入式伺服器之一。

Akka 是一個用於使用 Scala 或 Java 中的 Actor 模式構建應用程式的工具包,使用 Akka Streams 進行進程間通訊,並且內建了 Reactive Streams 合約。根據 David Karnok 的 反應式世代分類,Akka 是一個 "第三代" 函式庫。

為什麼是現在?

是什麼推動了企業 Java 中 Reactive 的興起?嗯,這不(全)僅僅是一種技術時尚 — 人們跳上潮流,使用閃亮的新玩具。驅動力是高效的資源利用,或者換句話說,在伺服器和資料中心上花費更少的錢。Reactive 的承諾是你可以用更少的資源做更多的事情,特別是你可以用更少的執行緒處理更高的負載。這就是 Reactive 和非阻塞、非同步 I/O 的交集出現的地方。對於正確的問題,效果是顯著的。對於錯誤的問題,效果可能會逆轉(你實際上會使事情變得更糟)。還要記住,即使你選擇了正確的問題,也沒有免費的午餐,並且 Reactive 不會為你解決問題,它只是給你一個工具箱,你可以用它來實現解決方案。

結論

在本文中,我們以非常廣泛和高層次的角度看待了 Reactive 運動,並將其置於現代企業的背景下。有許多用於 JVM 的 Reactive 函式庫或框架,所有這些都在積極開發中。在很大程度上,它們提供類似的功能,但由於 Reactive Streams,它們越來越具有互操作性。在本系列的 下一篇文章中,我們將深入探討一些實際的程式碼範例,以更好地了解成為 Reactive 的具體含義以及為什麼它很重要。我們還將花一些時間來理解 FRP 中的 "F" 為什麼很重要,以及反向壓力和非阻塞程式碼的概念如何對程式設計風格產生深遠的影響。最重要的是,我們將幫助你做出關於何時以及如何進行 Reactive 的重要決定,以及何時留在較舊的風格和堆疊上。

獲取 Spring 電子報

隨時關注 Spring 電子報

訂閱

搶先一步

VMware 提供培訓和認證,以加速你的進度。

了解更多

獲得支援

Tanzu Spring 在單一訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援與二進位檔。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部