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