響應式API的設計、實現和應用
在過去的幾年里,Java世界中在大力推動響應式編程的。無論是NodeJS開發人員使用非阻塞api的成功,還是引發延遲的微服務的爆炸式增長,還是僅僅是想要更有效地利用計算資源,許多開發人員都開始將響應式編程看作一種可行的編程模型。
幸運的是,涉及到響應式框架以及如何正確使用它們時,Java開發人員被選擇給寵壞了。沒有太多編寫響應式代碼的“錯誤”方法,但是,這同時也是問題所在;也沒多少編寫響應式代碼的“正確”方法。
在本文中,我們的目的是給你一些關于如何編寫響應式代碼的意見。這些觀點來自多年來開發一個大規模的響應式API的經驗,雖然它們可能并不適合你,但我們希望它們在你開始你的響應式之旅時能給你一些方向。
本文中的示例都來自于Cloud Foundry Java客戶端。這個項目使用Reactor項目的響應式框架。我們為這個Java客戶端選擇Reactor的原因,是因為它與Spring團隊有緊密的集成,但是我們討論的所有概念也都適用于其他的響應式框架,比如RxJava。如果你對Cloud Foundry有一些了解,這將很有幫助,但這不是必需的。這些例子有自解釋性命名,在解釋每個響應式概念時它們將助你更好地理解。 |
響應式編程是一個巨大的主題,它遠遠超出了本文的范圍,但是為了實現我們的目的,讓我們寬泛地把它定義為一種用更流暢的方式定義事件驅動系統的方法,而不是傳統的命令式編程風格。其目標是將命令式邏輯轉換為異步、非阻塞、函數式的樣式,這種樣式更容易理解和推理。
為這些做法(threads、NIO、callbacks等等)設計的命令式API并未考慮如何正確、可靠和方便地使用,許多情況下,在應用程序代碼中使用這些API仍需要大量顯式地管理。響應式框架的承諾是,這些關注點可以在幕后處理,從而讓開發人員能夠把主力精力放在應用程序功能代碼的編寫上。
我應該使用響應式編程嗎?
在設計響應式API時,首先要問自己的問題是,你是否想要一個響應式API! 響應式api不可能適用于所有的一切。響應式編程有顯而易見的缺點(目前最大的問題是調試,但框架和ide都正在積極解決此問題)。相反,當價值明顯大于缺點時,你就選擇響應式API吧。在作出這個判斷時,有幾個用于響應式編程的模式非常適合。
網絡化
網絡請求本質上就撇不開(相對)較大的延遲,而且等待這些響應返回通常是系統中最大的資源浪費。在非響應式應用程序中,那些等待中的請求通常會阻塞線程并消耗堆棧內存,空閑著等待響應到達。遠程故障和超時通常沒有得到系統地、明確地處理,因為提供的API不容易做到這一點。最后,遠程調用的負載通常是未知的、無邊界的,導致堆內存耗盡。響應式編程與非阻塞IO相結合,解決了這類問題,因為它為你提供了一個清晰的和顯式的API。
高并發操作
它也很適合用于協調高并發操作(如網絡請求或可并行化cpu密集型計算)。響應式框架,雖然允許顯式管理線程,但采用自動線程管理也很出色。像.flatmap()這樣的操作符透明地并行化行為,最大化地利用可用資源。
大規模可擴展應用
每個鏈接一個線程的servlet 模型已經為我們服務了很多年了。但是,隨著微服務的出現,我們已經開始看到應用程序大規模地擴展(25、50甚至100個單個無狀態應用程序的實例)來處理連接負載,即使CPU使用率處于空閑狀態。選擇非阻塞IO加響應式編程效果更佳,打破了鏈接與線程間的這種聯系,使可用資源得到更有效的利用。很明顯,這樣的優勢通常是驚人的。它常常需要在Tomcat上構建一個應用程序的更多實例,這些應用程序需要成百上千的線程來處理相同的負載,就像同一應用程序構建在擁有8個線程的Netty上一樣。
雖然以上所列不能完全用來評判響應式編程在哪里適用,但關鍵是要記住,如果你的應用不適合以上任何一種,那么你用它可能只是徒增復雜度,而不會增加任何價值。
響應式API應該返回什么?
如果你回答了第一個問題,判定出你的應用會從響應式API得到收益,那么就到了設計API的時候了。決定你的響應式API應該返回什么基本類型是一個好的起點。
Java世界中的所有響應式框架(包括Java 9的Flow)都是在響應式流程規范之上通信的。這個規范定義了一個低級的交互API,但是它不被認為是一個響應式框架(也就是說,它未針對流指定可用的操作符)。 |
在Reactor 項目中有兩種主要的類型。Flux
Flux<Application> listApplications() {...} Flux<String> listApplicationNames() { return listApplications() .map(Application::getName); } void printApplicationName() { listApplicationNames() .subscribe(System.out::println); }
在本例中,listApplications()方法執行一個網絡調用,并返回0到N個應用程序實例的Flux。然后,我們使用.map()操作符將每個應用程序轉換為其名稱的字符串。然后將以應用程序命名的Flux消費并輸出到控制臺。
Flux<Application> listApplications() {...} Mono<List<String>> listApplicationNames() { return listApplications() .map(Application::getName) .collectList(); } Mono<Boolean> doesApplicationExist(String name) { return listApplicationNames() .map(names -> names.contains(name)); }
Mono并不像Flux那樣有一個流,但是因為它們在概念上是一個元素的流,所以我們使用的操作符通常有相同的名稱。在這個例子中,除了映射到應用程序名稱的Flux之外,我們還將這些名稱收集到一個List中。在這種情況下,包含該列表的Mono可以被轉換為一個boolean值,表示其中是否包含某個名稱。這可能與直覺不符,但是如果你正在處理的項目在邏輯上是一個項目的集合,而不是它們的流,那么返回一個集合的Mono也很正常(例如Mono>)。
與命令式API不同,void不是一個適當的響應式返回類型。相反,每一個方法都必須返回一個Flux或者一個Mono。這可能看起來很奇怪(仍然有一些行為沒有任何返回呀!),但這是一個響應流基本操作的結果。調用響應式API的代碼執行(例如.flatmap ().map()…)是構建了一個數據到流的結構,但實際上并沒有轉換數據。只有在最后,當.subscribe()被調用時,數據才會開始向流轉換,并在隨之完成轉換。這種惰性執行正是為什么基于lambdas構建響應式編程的原因,以及為什么總要有返回類型,因為必須得有一些東西去.subscribe()。
void delete(String id) { this.restTemplate.delete(URI, id); } public void cleanup(String[] args) { delete("test-id"); }
上面這種的命令式阻塞示例可以返回void,因為它的網絡調用會立即開始執行,直到接收到響應時才返回。
Mono<Void> delete(String id) { return this.httpClient.delete(URI, id); } public void cleanup(String[] args) { CountDownLatch latch = new CountDownLatch(1); delete("test-id") .subscribe(n -> {}, Throwable::printStackTrace, () -> latch::countDown); latch.await(); }
在這個響應式示例中,網絡調用直到.subscribe()被調用后才開始,在delete()之后返回,因為它是用來生成調用的結構,而不是調用本身的結果。在本例中,我們使用返回0個條目的Mono
方法的范圍
一旦你決定了你的API需要返回什么,你就需要考慮你的每個方法(API和實現)將會做什么了。在該Java客戶端上,我們發現把方法設計小且可復用會帶來收益。它使每一種方法更容易組成更大的操作。這還能讓它們更靈活地組合成并行或順序操作。此外,它還使潛在的復雜流程更具可讀性。
Mono<ListApplicationsResponse> getPage(int page) { return this.client.applicationsV2() .list(ListApplicationsRequest.builder() .page(page) .build()); } void getResources() { getPage(1) .flatMapMany(response -> Flux.range(2, response.getTotalPages() - 1) .flatMap(page -> getPage(page)) .startWith(response)) .subscribe(System.out::println); }
這個例子演示了我們如何調用一個分頁的API。第一個getPage()請求檢索結果的第一頁。在結果的第一頁中包括我們需要檢索的頁面總數,以獲得完整的結果。因為getPage()方法是小的、可重用的,而且沒有其他額外作用,所以我們可以重用該方法,并可以通過totalPages并行為第2頁進行調用!
順序和并行協調
現在,幾乎所有顯著的性能改進都來自對并發性的提升。我們知道這一點,但許多系統的并發要么僅涉及傳入的連接,要么根本不并發。大部分這種情況都是源自這樣一個事實,那就是實現一個高度并發的系統又困難又容易出錯。響應式編程的一個重要優點是,你可以定義操作之間的順序和并行關系,并讓框架確定利用可用資源的最佳方式

責任編輯:售電衡衡
-
權威發布 | 新能源汽車產業頂層設計落地:鼓勵“光儲充放”,有序推進氫燃料供給體系建設
2020-11-03新能源,汽車,產業,設計 -
中國自主研制的“人造太陽”重力支撐設備正式啟運
2020-09-14核聚變,ITER,核電 -
探索 | 既耗能又可供能的數據中心 打造融合型綜合能源系統
2020-06-16綜合能源服務,新能源消納,能源互聯網
-
新基建助推 數據中心建設將迎爆發期
2020-06-16數據中心,能源互聯網,電力新基建 -
泛在電力物聯網建設下看電網企業數據變現之路
2019-11-12泛在電力物聯網 -
泛在電力物聯網建設典型實踐案例
2019-10-15泛在電力物聯網案例
-
權威發布 | 新能源汽車產業頂層設計落地:鼓勵“光儲充放”,有序推進氫燃料供給體系建設
2020-11-03新能源,汽車,產業,設計 -
中國自主研制的“人造太陽”重力支撐設備正式啟運
2020-09-14核聚變,ITER,核電 -
能源革命和電改政策紅利將長期助力儲能行業發展
-
探索 | 既耗能又可供能的數據中心 打造融合型綜合能源系統
2020-06-16綜合能源服務,新能源消納,能源互聯網 -
5G新基建助力智能電網發展
2020-06-125G,智能電網,配電網 -
從智能電網到智能城市