阿里專家杜萬:Java響應式編程,一文全面解讀

本篇文章來自於2018年12月22日舉辦的《阿里雲棲開發者沙龍—Java技術專場》,杜萬專家是該專場第四位演講的嘉賓,本篇文章是根據杜萬專家在《阿里雲棲開發者沙龍—Java技術專場》的演講視頻以及PPT整理而成。

摘要:響應式宣言如何解讀,Java中如何進行響應式編程,Reactor Streams又該如何使用?熱衷於整合框架與開發工具的阿里雲技術專家杜萬,爲大家全面解讀響應式編程,分享Spring Webflux的實踐。從響應式理解,到Reactor項目示例,再到Spring Webflux框架解讀,本文帶你進入Java響應式編程。

演講嘉賓簡介:
杜萬(倚賢),阿里雲技術專家,全棧工程師,從事了12年 Java 語言爲主的軟件開發工作,熱衷於整合框架與開發工具,Linux擁躉,問題終結者。合作翻譯《Elixir 程序設計》。目前負責阿里雲函數計算的工具鏈開發,正在實踐 WebFlux 和 Reactor 開發新的 Web 應用。

本次直播視頻精彩回顧,戳這裏!https://yq.aliyun.com/live/721
PPT下載地址:https://yq.aliyun.com/download/3187
以下內容根據演講嘉賓視頻分享以及PPT整理而成。

本文圍繞以下三部分進行介紹:
1.Reactive
2.Project Reactor
3.Spring Webflux

一.Reactive

1.Reactive Manifesto
下圖是Reactive Manifesto官方網站上的介紹,這篇文章非常短但也非常精悍,非常值得大家去認真閱讀。

響應式宣言是一份構建現代雲擴展架構的處方。這個框架主要使用消息驅動的方法來構建系統,在形式上可以達到彈性和韌性,最後可以產生響應性的價值。所謂彈性和韌性,通俗來說就像是橡皮筋,彈性是指橡皮筋可以拉長,而韌性指在拉長後可以縮回原樣。這裏爲大家一一解讀其中的關鍵詞:

1)響應性:快速/一致的響應時間。假設在有500個併發操作時,響應時間爲1s,那麼併發操作增長至5萬時,響應時間也應控制在1s左右。快速一致的響應時間才能給予用戶信心,是系統設計的追求。

2)韌性:複製/遏制/隔絕/委託。當某個模塊出現問題時,需要將這個問題控制在一定範圍內,這便需要使用隔絕的技術,避免連鎖性問題的發生。或是將出現故障部分的任務委託給其他模塊。韌性主要是系統對錯誤的容忍。

3)彈性:無競爭點或中心瓶頸/分片/擴展。如果沒有狀態的話,就進行水平擴展,如果存在狀態,就使用分片技術,將數據分至不同的機器上。

4)消息驅動:異步/鬆耦合/隔絕/地址透明/錯誤作爲消息/背壓/無阻塞。消息驅動是實現上述三項的技術支撐。其中,地址透明有很多方法。例如DNS提供的一串人類能讀懂的地址,而不是IP,這是一種不依賴於實現,而依賴於聲明的設計。再例如k8s每個service後會有多個Pod,依賴一個虛擬的服務而不是某一個真實的實例,從何實現調用1 個或調用n個服務實例對於對調用方無感知,這是爲分片或擴展做了準備。錯誤作爲消息,這在Java中是不太常見的,Java中通常將錯誤直接作爲異常拋出,而在響應式中,錯誤也是一種消息,和普通消息地位一致,這和JavaScript中的Promise類似。背壓是指當上遊向下遊推送數據時,可能下游承受能力不足導致問題,一個經典的比喻是就像用消防水龍頭解渴。因此下游需要向上遊聲明每次只能接受大約多少量的數據,當接受完畢再次向上遊申請數據傳輸。這便轉換成是下游向上遊申請數據,而不是上游向下遊推送數據。無阻塞是通過no-blocking IO提供更高的多線程切換效率。

2.Reactive Programming
響應式編程是一種聲明式編程範型。下圖中左側顯示了一個命令式編程,相信大家都比較熟悉。先聲明兩個變量,然後進行賦值,讓兩個變量相加,得到相加的結果。但接着當修改了最早聲明的兩個變量的值後,sum的值不會因此產生變化。而在Java 9 Flow中,按相同的思路實現上述處理流程,當初始變量的值變化,最後結果的值也同步發生變化,這就是響應式編程。這相當於聲明瞭一個公式,輸出值會隨着輸入值而同步變化。

響應式編程也是一種非阻塞的異步編程。下圖是用reactor.ipc.netty實現的TCP通信。常見的server中會用循環發數據後,在循環外取出,但在下圖的實現中沒有,因爲這不是使用阻塞模型實現,是基於非阻塞的異步編程實現。

響應式編程是一種數據流編程,關注於數據流而不是控制流。下圖中,首先當頁面出現點擊操作時產生一個click stream,然後頁面會將250ms內的clickStream緩存,如此實現了一個歸組過程。然後再進行map操作,得到每個list的長度,篩選出長度大於2的,這便可以得出多次點擊操作的流。這種方法應用非常廣泛,例如可以篩選出雙擊操作。由此可見,這種編程方式是一種數據流編程,而不是if else的控制流編程。

之前有提及消息驅動,那麼消息驅動(Message-driven)和事件驅動(Event-driven)有什麼區別呢。

1)消息驅動有確定的目標,一定會有消息的接受者,而事件驅動是一件事情希望被觀察到,觀察者是誰無關緊要。消息驅動系統關注消息的接受者,事件驅動系統關注事件源。

2)在一個使用響應式編程實現的響應式系統中,消息擅長於通訊,事件擅長於反應事實。

3.Reactive Streams
Reactive Streams提供了一套非阻塞背壓的異步流處理標準,主要應用在JVM、JavaScript和網絡協議工作中。通俗來說,它定義了一套響應式編程的標準。在Java中,有4個Reactive Streams API,如下圖所示:

這個API中定義了Publisher,即事件的發生源,它只有一個subscribe方法。其中的Subscriber就是訂閱消息的對象。

作爲訂閱者,有四個方法。onSubscribe會在每次接收消息時調用,得到的數據都會經過onNext方法。onError方法會在出現問題時調用,Throwable即是出現的錯誤消息。在結束時調用onComplete方法。

Subscription接口用來描述每個訂閱的消息。request方法用來向上遊索要指定個數的消息,cancel方法用於取消上游的數據推送,不再接受消息。

Processor接口繼承了Subscriber和Publisher,它既是消息的發生者也是消息的訂閱者。這是發生者和訂閱者間的過渡橋樑,負責一些中間轉換的處理。
Reactor Library從開始到現在已經歷經多代。第0代就是java包Observable 接口,也就是觀察者模式。具體的發展見下圖:

第四代雖然仍然是RxJava2,但是相比第三代的RxJava2,其中的小版本有了不一樣的改進,出現了新特性。
Reactor Library主要有兩點特性。一是基於回調(callback-based),在事件源附加回調函數,並在事件通過數據流鏈時被調用;二是聲明式編程(Declarative),很多函數處理業務類似,例如map/filter/fold等,這些操作被類庫固化後便可以使用聲明式方法,以在程序中快速便捷使用。在生產者、訂閱者都定義後,聲明式方法便可以用來實現中間處理者。

二.Project Reactor

Project Reactor,實現了完全非阻塞,並且基於網絡HTTP/TCP/UDP等的背壓,即數據傳輸上游爲網絡層協議時,通過遠程調用也可以實現背壓。同時,它還實現了Reactive Streams API和Reactive Extensions,以及支持Java 8 functional API/Completable Future/Stream /Duration等各新特性。下圖所示爲Reactor的一個示例:

首先定義了一個words的數組,然後使用flatMap做映射,再將每個詞和s做連接,得出的結果和另一個等長的序列進行一個zipWith操作,最後打印結果。這和Java 8 Stream非常類似,但仍存在一些區別:
1)Stream是pull-based,下游從上游拉數據的過程,它會有中間操作例如map和reduce,和終止操作例如collect等,只有在終止操作時纔會真正的拉取數據。Reactive是push-based,可以先將整個處理數據量構造完成,然後向其中填充數據,在出口處可以取出轉換結果。

2)Stream只能使用一次,因爲它是pull-based操作,拉取一次之後源頭不能更改。但Reactive可以使用多次,因爲push-based操作像是一個數據加工廠,只要填充數據就可以一直產出。

3)Stream#parallel()使用fork-join併發,就是將每一個大任務一直拆分至指定大小顆粒的小任務,每個小任務可以在不同的線程中執行,這種多線程模型符合了它的多核特性。Reactive使用Event loop,用一個單線程不停的做循環,每個循環處理有限的數據直至處理完成。

在上例中,大家可以看到很多Reactive的操作符,例如flatMap/concatWith/zipWith等,這樣的操作符有300多個,這可能是學習這個框架最大的壓力。如何理解如此繁多的操作符,可能一個歸類會有所幫助:

1)新序列創建,例如創建數組類序列等;
2)現有序列轉換,將其轉換爲新的序列,例如常見的map操作;
3)從現有的序列取出某些元素;
4)序列過濾;
5)序列異常處理。
6)與時間相關的操作,例如某個序列是由時間觸發器定期發起事件;
7)序列分割;
8)序列拉至同步世界,不是所有的框架都支持異步,再需要和同步操作進行交互時就需要這種處理。
上述300+操作符都有如下所示的彈珠圖(Marble Diagrams),用表意的方式解釋其作用。例如下圖的操作符是指,隨着時間推移,逐個產生了6個元素的序列,黑色豎線表示新元素產生終止。在這個操作符的作用下,下方只取了前三個元素,到第四個元素就不取了。這些彈珠圖大家可以自行了解。

三.Spring Webflux

1.Spring Webflux框架
Spring Boot 2.0相較之前的版本,在基於Spring Framework 5的構建添加了新模塊Webflux,將默認的web服務器改爲Netty,支持Reactive應用,並且Webflux默認運行在Netty上。而Spring Framework 5也有了一些變化。Java版本最低依賴Java 8,支持Java 9和Java 10,提供許多支持Reactive的基礎設施,提供面向Netty等運行時環境的適配器,新增Webflux模塊(集成的是Reactor 3.x)。下圖所示爲Webflux的框架:

左側是通常使用的框架,通過Servlet API的規範和Container進行交互,上一層是Spring-Webmvc,再上一層則是經常使用的一些註解。右側爲對應的Webflux層級,只要是支持NIO的Container,例如Tomcat,Jetty,Netty或Undertow都可以實現。在協議層的是HTTP/Reactive Streams。再上一層是Spring-Webflux,爲了保持兼容性,它支持這些常用的註解,同時也有一套新的語法規則Router Functions。下圖顯示了一個調用的實例:

在Client端,首先創建一個WebClient,調用其get方法,寫入URL,接收格式爲APPLICATION_STREAM_JSON的數據,retrieve獲得數據,取得數據後用bodyToFlux將數據轉換爲Car類型的對象,在doOnNext中打印構造好的Car對象,block方法意思是直到回調函數被執行纔可以結束。在Server端,在指定的path中進行get操作,produces和以前不同,這裏是application/stream+json,然後返回Flux範型的Car對象。傳統意義上,如果數據中有一萬條數據,那麼便直接返回一萬條數據,但在這個示例返回的Flux範型中,是不包含數據的,但在數據庫也支持Reactive的情況下,request可以一直往下傳遞,響應式的批量返回。傳統方式這樣的查詢很有可能是一個全表遍歷,這會需要較多資源和時間,甚至影響其他任務的執行。而響應式的方法除了可以避免這種情況,還可以讓用戶在第一時間看到數據而不是等待數據採集完畢,這在架構體驗的完整性上有了很大的提升。application/stream+json也是可以讓前端識別出,這些數據是分批響應式傳遞,而不會等待傳完才顯示。

現在的Java web應用可以使用Servlet棧或Reactive棧。Servlet棧已經有很久的使用歷史了,而現在又增加了更有優勢的Reactive棧,大家可以嘗試實現更好的用戶體驗。

2.Reactive編程模型
下圖中是Spring實現的一個向後兼容模型,可以使用annotation來標註Container。這是一個非常清晰、支持非常細節化的模型,也非常利於同事間的交流溝通。

下圖是一個Functional編程模型,通過寫函數的方式構造。例如下圖中傳入一個Request,返回Response,通過函數的方法重點關注輸入輸出,不需要區分狀態。然後將這些函數註冊至Route。這個模型和Node.js非常接近,也利於使用。

3.Spring Data框架
Spring Data框架支持多種數據庫,如下圖所示,最常用的是JPA和JDBC。在實踐中,不同的語言訪問不同的數據庫時,訪問接口是不一樣的,這對編程人員來說是個很大的工作量。

Spring Data便是做了另一層抽象,使你無論使用哪種數據庫,都可以使用同一個接口。具體特性這裏不做詳談。

下圖展示了一個Spring Data的使用示例。只需要寫一個方法簽名,然後註解爲Query,這個方法不需要實現,因爲框架後臺已經採用一些技術,直接根據findByFirstnameAndLastname就可以查詢到。這種一致的調用方式無疑提供了巨大的方便。

現在Reactive對Spring Data的支持還是不完整的,只支持了MongoDB/Redis/Cassandra和Couchbase,對JPA/LDAP/Elasticsearch/Neo4j/Solr等還不兼容。但也不是不能使用,例如對JDBC數據庫,將其轉爲同步即可使用,重點在於findAll和async兩個函數,這裏不再展開詳述,具體代碼如下圖所示:

Reactive不支持JDBC最根本的原因是,JDBC不是non-blocking設計。但是現在JavaOne已經在2016年9月宣佈了Non-blocking JDBC API的草案,雖然還未得到Java 10的支持,但可見這已經成爲一種趨勢。

四.總結

Spring MVC框架是一個命令式邏輯,方便編寫和調試。Spring WebFlux也具有衆多優勢,但調試卻不太容易,因爲它經常需要切換線程執行,出現錯誤的棧可能已經銷燬。當然這也是現今Java的編譯工具對WebFlux不太友好,相信以後會改善。下圖中列出了Spring MVC和Spring WebFlux各自的特性及交叉的部分。最後也附上一些參考資料。



本文作者:李博bluemind

閱讀原文

本文爲雲棲社區原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章