響應(yīng)式API的設(shè)計、實現(xiàn)和應(yīng)用
在過去的幾年里,Java世界中在大力推動響應(yīng)式編程的。無論是NodeJS開發(fā)人員使用非阻塞api的成功,還是引發(fā)延遲的微服務(wù)的爆炸式增長,還是僅僅是想要更有效地利用計算資源,許多開發(fā)人員都開始將響應(yīng)式編程看作一種可行的編程模型。
幸運的是,涉及到響應(yīng)式框架以及如何正確使用它們時,Java開發(fā)人員被選擇給寵壞了。沒有太多編寫響應(yīng)式代碼的“錯誤”方法,但是,這同時也是問題所在;也沒多少編寫響應(yīng)式代碼的“正確”方法。
在本文中,我們的目的是給你一些關(guān)于如何編寫響應(yīng)式代碼的意見。這些觀點來自多年來開發(fā)一個大規(guī)模的響應(yīng)式API的經(jīng)驗,雖然它們可能并不適合你,但我們希望它們在你開始你的響應(yīng)式之旅時能給你一些方向。
本文中的示例都來自于Cloud Foundry Java客戶端。這個項目使用Reactor項目的響應(yīng)式框架。我們?yōu)檫@個Java客戶端選擇Reactor的原因,是因為它與Spring團隊有緊密的集成,但是我們討論的所有概念也都適用于其他的響應(yīng)式框架,比如RxJava。如果你對Cloud Foundry有一些了解,這將很有幫助,但這不是必需的。這些例子有自解釋性命名,在解釋每個響應(yīng)式概念時它們將助你更好地理解。 |
響應(yīng)式編程是一個巨大的主題,它遠(yuǎn)遠(yuǎn)超出了本文的范圍,但是為了實現(xiàn)我們的目的,讓我們寬泛地把它定義為一種用更流暢的方式定義事件驅(qū)動系統(tǒng)的方法,而不是傳統(tǒng)的命令式編程風(fēng)格。其目標(biāo)是將命令式邏輯轉(zhuǎn)換為異步、非阻塞、函數(shù)式的樣式,這種樣式更容易理解和推理。
為這些做法(threads、NIO、callbacks等等)設(shè)計的命令式API并未考慮如何正確、可靠和方便地使用,許多情況下,在應(yīng)用程序代碼中使用這些API仍需要大量顯式地管理。響應(yīng)式框架的承諾是,這些關(guān)注點可以在幕后處理,從而讓開發(fā)人員能夠把主力精力放在應(yīng)用程序功能代碼的編寫上。
我應(yīng)該使用響應(yīng)式編程嗎?
在設(shè)計響應(yīng)式API時,首先要問自己的問題是,你是否想要一個響應(yīng)式API! 響應(yīng)式api不可能適用于所有的一切。響應(yīng)式編程有顯而易見的缺點(目前最大的問題是調(diào)試,但框架和ide都正在積極解決此問題)。相反,當(dāng)價值明顯大于缺點時,你就選擇響應(yīng)式API吧。在作出這個判斷時,有幾個用于響應(yīng)式編程的模式非常適合。
網(wǎng)絡(luò)化
網(wǎng)絡(luò)請求本質(zhì)上就撇不開(相對)較大的延遲,而且等待這些響應(yīng)返回通常是系統(tǒng)中最大的資源浪費。在非響應(yīng)式應(yīng)用程序中,那些等待中的請求通常會阻塞線程并消耗堆棧內(nèi)存,空閑著等待響應(yīng)到達(dá)。遠(yuǎn)程故障和超時通常沒有得到系統(tǒng)地、明確地處理,因為提供的API不容易做到這一點。最后,遠(yuǎn)程調(diào)用的負(fù)載通常是未知的、無邊界的,導(dǎo)致堆內(nèi)存耗盡。響應(yīng)式編程與非阻塞IO相結(jié)合,解決了這類問題,因為它為你提供了一個清晰的和顯式的API。
高并發(fā)操作
它也很適合用于協(xié)調(diào)高并發(fā)操作(如網(wǎng)絡(luò)請求或可并行化cpu密集型計算)。響應(yīng)式框架,雖然允許顯式管理線程,但采用自動線程管理也很出色。像.flatmap()這樣的操作符透明地并行化行為,最大化地利用可用資源。
大規(guī)模可擴展應(yīng)用
每個鏈接一個線程的servlet 模型已經(jīng)為我們服務(wù)了很多年了。但是,隨著微服務(wù)的出現(xiàn),我們已經(jīng)開始看到應(yīng)用程序大規(guī)模地擴展(25、50甚至100個單個無狀態(tài)應(yīng)用程序的實例)來處理連接負(fù)載,即使CPU使用率處于空閑狀態(tài)。選擇非阻塞IO加響應(yīng)式編程效果更佳,打破了鏈接與線程間的這種聯(lián)系,使可用資源得到更有效的利用。很明顯,這樣的優(yōu)勢通常是驚人的。它常常需要在Tomcat上構(gòu)建一個應(yīng)用程序的更多實例,這些應(yīng)用程序需要成百上千的線程來處理相同的負(fù)載,就像同一應(yīng)用程序構(gòu)建在擁有8個線程的Netty上一樣。
雖然以上所列不能完全用來評判響應(yīng)式編程在哪里適用,但關(guān)鍵是要記住,如果你的應(yīng)用不適合以上任何一種,那么你用它可能只是徒增復(fù)雜度,而不會增加任何價值。
響應(yīng)式API應(yīng)該返回什么?
如果你回答了第一個問題,判定出你的應(yīng)用會從響應(yīng)式API得到收益,那么就到了設(shè)計API的時候了。決定你的響應(yīng)式API應(yīng)該返回什么基本類型是一個好的起點。
Java世界中的所有響應(yīng)式框架(包括Java 9的Flow)都是在響應(yīng)式流程規(guī)范之上通信的。這個規(guī)范定義了一個低級的交互API,但是它不被認(rèn)為是一個響應(yīng)式框架(也就是說,它未針對流指定可用的操作符)。 |
在Reactor 項目中有兩種主要的類型。Flux
Flux<Application> listApplications() {...} Flux<String> listApplicationNames() { return listApplications() .map(Application::getName); } void printApplicationName() { listApplicationNames() .subscribe(System.out::println); }
在本例中,listApplications()方法執(zhí)行一個網(wǎng)絡(luò)調(diào)用,并返回0到N個應(yīng)用程序?qū)嵗腇lux。然后,我們使用.map()操作符將每個應(yīng)用程序轉(zhuǎn)換為其名稱的字符串。然后將以應(yīng)用程序命名的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那樣有一個流,但是因為它們在概念上是一個元素的流,所以我們使用的操作符通常有相同的名稱。在這個例子中,除了映射到應(yīng)用程序名稱的Flux之外,我們還將這些名稱收集到一個List中。在這種情況下,包含該列表的Mono可以被轉(zhuǎn)換為一個boolean值,表示其中是否包含某個名稱。這可能與直覺不符,但是如果你正在處理的項目在邏輯上是一個項目的集合,而不是它們的流,那么返回一個集合的Mono也很正常(例如Mono>)。
與命令式API不同,void不是一個適當(dāng)?shù)捻憫?yīng)式返回類型。相反,每一個方法都必須返回一個Flux或者一個Mono。這可能看起來很奇怪(仍然有一些行為沒有任何返回呀!),但這是一個響應(yīng)流基本操作的結(jié)果。調(diào)用響應(yīng)式API的代碼執(zhí)行(例如.flatmap ().map()…)是構(gòu)建了一個數(shù)據(jù)到流的結(jié)構(gòu),但實際上并沒有轉(zhuǎn)換數(shù)據(jù)。只有在最后,當(dāng).subscribe()被調(diào)用時,數(shù)據(jù)才會開始向流轉(zhuǎn)換,并在隨之完成轉(zhuǎn)換。這種惰性執(zhí)行正是為什么基于lambdas構(gòu)建響應(yīng)式編程的原因,以及為什么總要有返回類型,因為必須得有一些東西去.subscribe()。
void delete(String id) { this.restTemplate.delete(URI, id); } public void cleanup(String[] args) { delete("test-id"); }
上面這種的命令式阻塞示例可以返回void,因為它的網(wǎng)絡(luò)調(diào)用會立即開始執(zhí)行,直到接收到響應(yīng)時才返回。
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(); }
在這個響應(yīng)式示例中,網(wǎng)絡(luò)調(diào)用直到.subscribe()被調(diào)用后才開始,在delete()之后返回,因為它是用來生成調(diào)用的結(jié)構(gòu),而不是調(diào)用本身的結(jié)果。在本例中,我們使用返回0個條目的Mono
方法的范圍
一旦你決定了你的API需要返回什么,你就需要考慮你的每個方法(API和實現(xiàn))將會做什么了。在該Java客戶端上,我們發(fā)現(xiàn)把方法設(shè)計小且可復(fù)用會帶來收益。它使每一種方法更容易組成更大的操作。這還能讓它們更靈活地組合成并行或順序操作。此外,它還使?jié)撛诘膹?fù)雜流程更具可讀性。
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); }
這個例子演示了我們?nèi)绾握{(diào)用一個分頁的API。第一個getPage()請求檢索結(jié)果的第一頁。在結(jié)果的第一頁中包括我們需要檢索的頁面總數(shù),以獲得完整的結(jié)果。因為getPage()方法是小的、可重用的,而且沒有其他額外作用,所以我們可以重用該方法,并可以通過totalPages并行為第2頁進(jìn)行調(diào)用!
順序和并行協(xié)調(diào)
現(xiàn)在,幾乎所有顯著的性能改進(jìn)都來自對并發(fā)性的提升。我們知道這一點,但許多系統(tǒng)的并發(fā)要么僅涉及傳入的連接,要么根本不并發(fā)。大部分這種情況都是源自這樣一個事實,那就是實現(xiàn)一個高度并發(fā)的系統(tǒng)又困難又容易出錯。響應(yīng)式編程的一個重要優(yōu)點是,你可以定義操作之間的順序和并行關(guān)系,并讓框架確定利用可用資源的最佳方式
責(zé)任編輯:售電衡衡
-
權(quán)威發(fā)布 | 新能源汽車產(chǎn)業(yè)頂層設(shè)計落地:鼓勵“光儲充放”,有序推進(jìn)氫燃料供給體系建設(shè)
2020-11-03新能源,汽車,產(chǎn)業(yè),設(shè)計 -
中國自主研制的“人造太陽”重力支撐設(shè)備正式啟運
2020-09-14核聚變,ITER,核電 -
探索 | 既耗能又可供能的數(shù)據(jù)中心 打造融合型綜合能源系統(tǒng)
2020-06-16綜合能源服務(wù),新能源消納,能源互聯(lián)網(wǎng)
-
新基建助推 數(shù)據(jù)中心建設(shè)將迎爆發(fā)期
2020-06-16數(shù)據(jù)中心,能源互聯(lián)網(wǎng),電力新基建 -
泛在電力物聯(lián)網(wǎng)建設(shè)下看電網(wǎng)企業(yè)數(shù)據(jù)變現(xiàn)之路
2019-11-12泛在電力物聯(lián)網(wǎng) -
泛在電力物聯(lián)網(wǎng)建設(shè)典型實踐案例
2019-10-15泛在電力物聯(lián)網(wǎng)案例
-
新基建之充電樁“火”了 想進(jìn)這個行業(yè)要“心里有底”
2020-06-16充電樁,充電基礎(chǔ)設(shè)施,電力新基建 -
燃料電池汽車駛?cè)雽こ0傩占疫€要多久?
-
備戰(zhàn)全面電動化 多部委及央企“定調(diào)”充電樁配套節(jié)奏
-
權(quán)威發(fā)布 | 新能源汽車產(chǎn)業(yè)頂層設(shè)計落地:鼓勵“光儲充放”,有序推進(jìn)氫燃料供給體系建設(shè)
2020-11-03新能源,汽車,產(chǎn)業(yè),設(shè)計 -
中國自主研制的“人造太陽”重力支撐設(shè)備正式啟運
2020-09-14核聚變,ITER,核電 -
能源革命和電改政策紅利將長期助力儲能行業(yè)發(fā)展
-
探索 | 既耗能又可供能的數(shù)據(jù)中心 打造融合型綜合能源系統(tǒng)
2020-06-16綜合能源服務(wù),新能源消納,能源互聯(lián)網(wǎng) -
5G新基建助力智能電網(wǎng)發(fā)展
2020-06-125G,智能電網(wǎng),配電網(wǎng) -
從智能電網(wǎng)到智能城市