領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多響應式程式設計(Reactive Programming)又一次引起了人們的興趣,目前關於它的討論非常多,但對於像作者這樣的外部人士和普通的企業 Java 開發人員來說,這些討論並不那麼容易理解。本文(系列文章的第一篇)可能有助於澄清你對這場喧囂的理解。本文的方法儘可能具體,並且不會提及“指示性語義”。如果你正在尋找更學術的方法和大量的 Haskell 程式碼示例,網際網路上有很多,但你可能不應該在這裡。
響應式程式設計常常與併發程式設計和高效能混淆,以至於很難將這些概念區分開來,而實際上它們原則上是完全不同的。這不可避免地導致了混淆。響應式程式設計也經常被稱為或混淆為函式式響應式程式設計(Functional Reactive Programming,簡稱 FRP),(在本文中我們互換使用這兩個術語)。有些人認為響應式程式設計沒什麼新鮮的,他們每天都在做(主要是使用 JavaScript)。另一些人似乎認為它是微軟的一項饋贈(微軟在不久前釋出了一些 C# 擴充套件時,曾為此大肆宣傳)。在企業 Java 領域,最近出現了一些動靜(例如,請參閱 Reactive Streams initiative),正如任何閃亮的新事物一樣,在何時何地可以使用和應該使用它方面,有很多容易犯的錯誤。
響應式程式設計是一種微架構風格,涉及智慧的事件路由和消費,所有這些共同作用以改變行為。這有點抽象,而且你將在網上遇到的許多其他定義也是如此。在接下來的內容中,我們將嘗試建立一些更具體的概念,以說明響應式意味著什麼,或者為什麼它可能很重要。
響應式程式設計的起源可能可以追溯到 20 世紀 70 年代甚至更早,所以這個想法並不新鮮,但它卻與現代企業中的一些事物產生了共鳴。這種共鳴(並非偶然)與微服務的興起和多核處理器的普及同時發生。希望其中一些原因將在後面變得清晰。
以下是一些來自其他來源的有用的小定義
響應式程式設計的基本思想是,存在某些資料型別,它們代表“隨時間變化”的值。涉及這些隨時間變化的值的計算本身也會產生隨時間變化的值。
還有…
要獲得對此的初步直觀認識,一個簡單的方法是想象你的程式是一個電子表格,而你的所有變數都是單元格。如果電子表格中的任何一個單元格發生變化,任何引用該單元格的單元格也會隨之變化。這與 FRP 是一樣的。現在想象一下,有些單元格是自己變化的(或者更確切地說,是從外部世界獲取的):在 GUI 環境中,滑鼠的位置就是一個很好的例子。
(來自 Stackoverflow 上的術語問題)
FRP 與高效能、併發、非同步操作和非阻塞 I/O 具有很強的親和性。然而,從懷疑 FRP 與這些完全無關的角度開始可能是有益的。當使用響應式模型時,這些問題當然可以被自然地處理,通常對呼叫者來說是透明的。但是,在有效或高效處理這些問題方面的實際好處完全取決於具體的實現(因此應該受到高度審查)。也可以實現一個完全合理且有用的 FRP 框架,採用同步、單執行緒的方式,但這在嘗試使用任何新工具和庫時可能並沒有什麼幫助。
對於新手來說,最難回答的問題似乎是“它有什麼用?”以下是一些企業場景的示例,說明了其通用使用模式
外部服務呼叫 當今許多後端服務都是 RESTful 的(即,它們透過 HTTP 執行),因此底層協議本質上是阻塞的和同步的。對於 FRP 來說,這可能不是一個顯而易見的應用領域,但實際上它是一個相當肥沃的領域,因為實現此類服務通常涉及呼叫其他服務,然後根據第一個呼叫的結果又呼叫更多服務。由於有大量的 I/O 操作,如果你要等待一個呼叫完成,然後才傳送下一個請求,那麼在你能組合出一個回覆之前,可憐的客戶端就會因為沮喪而放棄。因此,外部服務呼叫,特別是複雜的依賴關係編排,是需要最佳化的好地方。FRP 提供了驅動這些操作的邏輯的“可組合性”的承諾,從而使呼叫服務的開發人員更容易編寫程式碼。
高併發訊息消費者 訊息處理,特別是高併發的訊息處理,是企業中常見的用例。響應式框架喜歡衡量微基準測試,並吹噓在 JVM 中每秒可以處理多少條訊息。結果確實驚人(每秒數千萬條訊息很容易實現),但可能有些人為——如果它們說在對一個簡單的“for”迴圈進行基準測試,你可能就不會那麼印象深刻了。然而,我們不應該過早地否定這類工作,並且很容易看到,當效能很重要時,所有的貢獻都應該被感激地接受。響應式模式很自然地適合訊息處理(因為事件可以很好地轉換為訊息),所以如果有一種方法可以更快地處理更多訊息,我們應該引起注意。
電子表格 這可能不是一個真正的企業用例,但每個企業中的人都能輕易地理解它,而且它很好地捕捉了 FRP 的理念和實現難度。如果單元格 B 依賴於單元格 A,而單元格 C 依賴於單元格 A 和 B,那麼如何傳播 A 的變化,確保在將任何更改事件傳送到 B 之前更新 C?如果你有一個真正響應式的框架來構建,那麼答案是“你不用關心,你只需要宣告依賴關係”,這實際上就是電子表格的強大之處。它也突顯了 FRP 和簡單的事件驅動程式設計之間的區別——它在“智慧路由”中賦予了“智慧”的含義。
對(非同步)處理的抽象 這是一個更抽象的用例,因此涉及到我們可能應該避免的領域。這與前面提到的更具體的用例也有一些(很多)重疊,但希望它仍然值得討論。基本主張是熟悉且合理的:只要開發人員願意接受額外的抽象層,他們就可以忘記他們呼叫的程式碼是同步的還是非同步的。由於處理非同步程式設計會消耗寶貴的腦力,所以這其中可能有一些有用的想法。響應式程式設計不是解決這個問題的唯一方法,但一些 FRP 的實現者已經深入思考了這個問題,以至於他們的工具非常有用。
這個 Netflix 部落格提供了一些非常實用的實際用例的示例:Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava
如果你從 1970 年起就沒有隱居起來,那麼你一定會遇到一些與響應式程式設計及其試圖解決的問題相關的概念。這裡有幾個,以及我對它們相關性的個人看法
Ruby Event-Machine Event Machine 是對併發程式設計(通常涉及非阻塞 I/O)的一種抽象。Rubyist 花了很長時間才將一種專為單執行緒指令碼設計的語言轉變為可以用來編寫a)工作正常、b)效能良好、c)在負載下保持執行的伺服器應用程式。Ruby 已經有了執行緒一段時間,但它們並不常被使用,而且聲譽不佳,因為它們並不總是表現良好。替代方案是現在已經推廣到語言核心(在 Ruby 1.9 中)的Fibers( sic)。Fiber 程式設計模型是一種協程(coroutines)的變體(見下文),其中單個原生執行緒用於處理大量併發請求(通常涉及 I/O)。程式設計模型本身有點抽象,難以推理,所以大多數人使用包裝器,而 Event Machine 是最常見的。Event Machine 不一定使用 Fibers(它抽象了這些關注點),但在 Ruby Web 應用中很容易找到使用 Event Machine 和 Fibers 的程式碼示例(例如,參見 Ilya Grigorik 的這篇文章,或者em-http-request 中的 fibered 示例)。人們這樣做是為了在 I/O 密集型應用程式中使用 Event Machine 獲得可擴充套件性的好處,而無需使用大量巢狀回撥的醜陋程式設計模型。
Actor 模型 類似於面向物件程式設計,Actor 模型是計算機科學的一個深度分支,可以追溯到 20 世紀 70 年代。Actor 提供了一種計算(而不是資料和行為)的抽象,允許併發作為其自然結果,因此在實踐中它們可以構成併發系統的基礎。Actor 之間傳送訊息,所以它們在某種意義上是響應式的,並且自稱為 Actor 或 Reactive 的系統之間有很多重疊。通常區別在於它們的實現級別(例如,Akka 中的 `Actors` 可以分佈在不同的程序中,這是該框架的一個顯著特徵)。
延遲結果(Futures) Java 1.5 引入了一套豐富的新庫,包括 Doug Lea 的“java.util.concurrent”,其中包含了一個延遲結果的概念,封裝在 `Future` 中。它是一個在非同步模式之上進行簡單抽象的良好示例,而不強制實現必須是非同步的,或使用任何特定的非同步處理模型。正如 Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava 中所展示的那樣,當您只需要併發處理一組類似的任務時,`Futures` 非常有用,但一旦其中任何一個任務需要相互依賴或有條件地執行,您就會陷入一種“巢狀回撥地獄”。響應式程式設計為這種困境提供瞭解藥。
Map-reduce 和 fork-join 並行處理的抽象很有用,並且有很多例子可供選擇。Map-reduce 和 fork-join 最近在 Java 世界中得到發展,這得益於大規模並行分散式處理(MapReduce 和 Hadoop)以及 JDK 本身在 1.7 版本中(Fork-Join)。這些是實用的抽象,但(與延遲結果一樣)它們與 FRP 相比很淺顯,FRP 可以用作簡單並行處理的抽象,但它超越了這一點,達到了可組合性和宣告式通訊。
協程 “協程”(coroutine)是“子例程”(subroutine)的泛化——它有一個入口點和一個或多個出口點,就像子例程一樣,但當它退出時,它會將控制權傳遞給另一個協程(不一定是其呼叫者),並且它積累的任何狀態都會被儲存並記住,以便下次呼叫時使用。協程可以用作 Actor 和 Streams 等更高階功能的構建塊。響應式程式設計的目標之一是為通訊的並行處理代理提供相同型別的抽象,因此協程(如果可用)是有用的構建塊。協程有各種變體,其中一些比一般情況更受限制,但比普通的子例程更靈活。Fibers(參見關於 Event Machine 的討論)是一種變體,而 Generators(Scala 和 Python 中很熟悉)是另一種。
Java 本身並不是一種“響應式語言”,因為它不原生支援協程。JVM 上還有其他語言(Scala 和 Clojure)更原生支援響應式模型,但 Java 本身直到 9.0 版本才支援。然而,Java 是企業開發的強大力量,最近在 JDK 之上提供響應式層方面有很多活動。我們在這裡只對其中一些進行非常簡要的介紹。
Reactive Streams 是一個非常底層的契約,表示為 handful 的 Java 介面(加上一個 TCK),但也可應用於其他語言。這些介面透過顯式的反壓(back pressure)表達了 `Publisher` 和 `Subscriber` 的基本構建塊,為可互操作的庫形成了一種通用語言。Reactive Streams 已在 9.0 版本中作為 `java.util.concurrent.Flow` 被納入 JDK。該專案是 Kaazing、Netflix、Pivotal、Red Hat、Twitter、Typesafe 等眾多工程師的合作成果。
RxJava:Netflix 在內部使用響應式模式已有一段時間,然後他們將使用的工具以開源許可證的形式釋出,即 Netflix/RxJava(隨後更名為“ReactiveX/RxJava”)。Netflix 使用 RxJava 進行大量的 Groovy 程式設計,但它也支援 Java,並且透過 Lambda 非常適合 Java 8。有一個連線到 Reactive Streams 的橋接器。根據 David Karnok 的《Reactive 的代際》分類,RxJava 是一個“第二代”庫。
Reactor 是來自 Pivotal 開源團隊(建立 Spring 的團隊)的 Java 框架。它直接構建在 Reactive Streams 之上,因此無需橋接器。Reactor IO 專案提供了對 Netty 和 Aeron 等底層網路執行時的包裝。根據 David Karnok 的《Reactive 的代際》分類,Reactor 是一個“第四代”庫。
Spring Framework 5.0(2016 年 6 月釋出第一個里程碑)在其內部構建了響應式功能,包括用於構建 HTTP 伺服器和客戶端的工具。Spring 在 Web 層的現有使用者會發現一個非常熟悉的程式設計模型,使用註解來裝飾控制器方法以處理 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 的《Reactive 的代際》分類,Akka 是一個“第三代”庫。
是什麼推動了響應式在企業 Java 中的興起?嗯,這(不完全)只是一種技術潮流——人們只是搭上了新潮玩具的順風車。驅動力是有效的資源利用,換句話說,就是減少在伺服器和資料中心上的花費。響應式的承諾是,你可以用更少的資源做更多的事情,特別是你可以用更少的執行緒處理更高的負載。這就是響應式與非阻塞、非同步 I/O 相交的地方。對於正確的問題,效果是驚人的。對於錯誤的問題,效果可能會適得其反(你實際上會讓事情變得更糟)。還要記住,即使你選擇了正確的問題,天下也沒有免費的午餐,響應式並不能為你解決問題,它只是提供了一個你可以用來實現解決方案的工具箱。
在本文中,我們對響應式運動進行了非常廣泛和高層次的概述,將其置於現代企業的背景下。JVM 上有許多響應式庫或框架,都處於積極開發中。在很大程度上,它們提供了相似的功能,但由於 Reactive Streams 的存在,它們越來越互操作。在下一篇文章中,我們將深入探討實際程式碼示例,以更清楚地瞭解響應式意味著什麼以及為什麼它很重要。我們還將花一些時間來理解為什麼 FRP 中的“F”很重要,以及反壓和非阻塞程式碼的概念如何深刻影響程式設計風格。最重要的是,我們將幫助您做出重要決定,何時以及如何採用響應式,以及何時保持使用舊的樣式和堆疊。