本篇文章來自于2018年12月22日舉辦的
《阿裡雲栖開發者沙龍—Java技術專場》,杜萬專家是該專場第四位演講的嘉賓,本篇文章是根據杜萬專家在
的演講視訊以及PPT整理而成。
摘要:響應式宣言如何解讀,Java中如何進行響應式程式設計,Reactor Streams又該如何使用?熱衷于整合架構與開發工具的阿裡雲技術專家杜萬,為大家全面解讀響應式程式設計,分享Spring Webflux的實踐。從響應式了解,到Reactor項目示例,再到Spring Webflux架構解讀,本文帶你進入Java響應式程式設計。
演講嘉賓簡介:
杜萬(倚賢),阿裡雲技術專家,全棧工程師,從事了12年 Java 語言為主的軟體開發工作,熱衷于整合架構與開發工具,Linux擁趸,問題終結者。合作翻譯《Elixir 程式設計》。目前負責阿裡雲函數計算的工具鍊開發,正在實踐 WebFlux 和 Reactor 開發新的 Web 應用。
本次直播視訊精彩回顧,戳這裡!
https://yq.aliyun.com/live/721PPT下載下傳位址:
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各自的特性及交叉的部分。最後也附上一些參考資料。
社群技術交流:【阿裡Java技術進階】每周在群内進行【技術教育訓練直播】和【線上回答技術問題】歡迎點選link入群:
http://tb.cn/gXRstIw或者 釘釘掃碼入群:
相關文章:
阿裡雲栖開發者沙龍-Java技術專場 (最全資料下載下傳) 【Java開發者專場】阿裡專家梁笑:2018雙十一下單成功率99.9%!供應鍊服務平台如何迎接大促 【Java開發者專場】阿裡專家墨玖:淘票票工程師文化 【Java開發者專場】阿裡特邀專家徐雷:Java為王,網際網路高并發架構設計選型之路