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

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

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

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

本次直播視頻精彩回顧,戳這裏!
PPT下載地址:戳這裏!
如下內容根據演講嘉賓視頻分享以及PPT整理而成。web

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

一.Reactive

1.Reactive Manifesto
下圖是Reactive Manifesto官方網站上的介紹,這篇文章很是短但也很是精悍,很是值得你們去認真閱讀。編程

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

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

閱讀原文

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

相關文章
相關標籤/搜索