【Java開發者專場】阿里專家杜萬: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)響應性:
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各自的特性及交叉的部分。最後也附上一些參考資料。
社群技術交流:【阿里Java技術進階】每週在群內進行【技術培訓直播】和【線上回答技術問題】歡迎點選link入群: http://tb.cn/gXRstIw
或者 釘釘掃碼入群:
相關文章:
阿里雲棲開發者沙龍-Java技術專場 (最全資料下載)
【Java開發者專場】阿里專家樑笑:2018雙十一下單成功率99.9%!供應鏈服務平臺如何迎接大促
【Java開發者專場】阿里專家墨玖:淘票票工程師文化
【Java開發者專場】阿里特邀專家徐雷:Java為王,網際網路高併發架構設計選型之路