一文讀懂響應(yīng)式編程到底是什么?
最近幾年,隨著Go、Node 等新語言、新技術(shù)的出現(xiàn),Java 作為服務(wù)器端開發(fā)語言老大的地位受到了不小的挑戰(zhàn)。雖然Java 的市場(chǎng)地位在短時(shí)間內(nèi)并不會(huì)發(fā)生改變,但Java 社區(qū)還是將挑戰(zhàn)視為機(jī)遇,并努力、不斷地提高自身應(yīng)對(duì)高并發(fā)服務(wù)器端開發(fā)場(chǎng)景的能力。
為了應(yīng)對(duì)高并發(fā)服務(wù)器端開發(fā)場(chǎng)景,在2009 年,微軟提出了一個(gè)更優(yōu)雅地實(shí)現(xiàn)異步編程的方式—— Reactive Programming ,我們稱之為響應(yīng)式編程。
隨后,各語言很快跟進(jìn),都擁有了屬于自己的響應(yīng)式編程實(shí)現(xiàn)。比如,JavaScript 語言就在ES6 中通過Promise 機(jī)制引入了類似的異步編程方式。同時(shí),Java 社區(qū)也在快速發(fā)展,Netflix 和LightBend 公司提供了RxJava 和Akka Stream 等技術(shù),使得Java 平臺(tái)也有了能夠?qū)崿F(xiàn)響應(yīng)式編程的框架。
當(dāng)下,我們通過Mina 和Netty 這樣的NIO 框架其實(shí)就能完成高并發(fā)下的服務(wù)器端開發(fā)任務(wù),但這樣的技術(shù)只掌握在少數(shù)高級(jí)開發(fā)人員手中,因?yàn)樗鼈冸y度較大,并不適合大部分普通開發(fā)者。
雖然目前已經(jīng)有不少公司在實(shí)踐響應(yīng)式編程,但整體來說,其應(yīng)用范圍依舊不大。出現(xiàn)這種情況的原因在于當(dāng)下缺少簡(jiǎn)單、易用的技術(shù),這些技術(shù)需要能使響應(yīng)式編程更加普及,并做到如同Spring MVC 一樣結(jié)合Spring 提供的服務(wù)對(duì)各種技術(shù)進(jìn)行整合。
在2017 年9 月28 日,Spring 5 正式發(fā)布。Spring 5 發(fā)布最大的意義在于,它將響應(yīng)式編程技術(shù)的普及向前推進(jìn)了一大步。而同時(shí),作為在背后支持Spring 5 響應(yīng)式編程的框架Spring Reactor,也進(jìn)入了里程碑式的3.1.0 版本。
響應(yīng)式編程到底是什么?
在現(xiàn)實(shí)生活中,當(dāng)我們聽到有人喊我們名字的時(shí)候,會(huì)對(duì)其進(jìn)行響應(yīng),也就是說,我們是基于事件驅(qū)動(dòng)模式來進(jìn)行編程的。所以這個(gè)過程其實(shí)就是下發(fā)產(chǎn)生的事件,然后我們作為消費(fèi)者對(duì)下發(fā)事件進(jìn)行一系列的消費(fèi)。
從這個(gè)角度來說,對(duì)整個(gè)代碼的設(shè)計(jì)應(yīng)該是針對(duì)消費(fèi)者來進(jìn)行的。比如,看電影,有些畫面我們不想看,那就閉上眼睛;有些聲音不想聽,那就捂上耳朵。其實(shí)這就是對(duì)消費(fèi)者的增強(qiáng)包裝,我們把復(fù)雜的邏輯拆分開,然后將其分割成一個(gè)個(gè)小任務(wù)進(jìn)行封裝,于是就有了諸如filter、map、skip、limit 等操作。
01
并發(fā)與并行的關(guān)系
可以說,并發(fā)很好地利用了CPU 時(shí)間片的特性,也就是操作系統(tǒng)選擇并運(yùn)行一個(gè)任務(wù),接著在下一個(gè)時(shí)間片內(nèi)運(yùn)行另一個(gè)任務(wù),并把前一個(gè)任務(wù)設(shè)置成等待狀態(tài)。其實(shí)并發(fā)并不意味著并行。
具體列舉下面幾種情況。
① 有時(shí)候,多線程執(zhí)行會(huì)提高應(yīng)用程序的性能,而有時(shí)候反而會(huì)降低應(yīng)用程序的性能。這在 JDK 中Stream API 的使用上體現(xiàn)得很明顯,如果任務(wù)量很小,而我們又使用了并行流,反而降低了應(yīng)用程序的性能。
② 在多線程編程中,可能會(huì)同時(shí)開啟或者關(guān)閉多個(gè)線程,這樣會(huì)產(chǎn)生很大的性能開銷, 也降低了應(yīng)用程序的性能。
③ 當(dāng)線程同時(shí)處于等待I/O 的過程中時(shí),并發(fā)可能會(huì)阻塞CPU 資源,其后果不僅是用戶長(zhǎng)時(shí)間等待,而且會(huì)浪費(fèi)CPU 的計(jì)算資源。
④ 如果幾個(gè)線程共享了一個(gè)數(shù)據(jù),情況就會(huì)變得有些復(fù)雜。我們需要考慮數(shù)據(jù)在各個(gè)線程中的狀態(tài)是否一致。為了達(dá)到數(shù)據(jù)一致的目的,很可能會(huì)使用synchronized 或者lock 相關(guān)操作。
現(xiàn)在,你對(duì)并發(fā)有一定的了解了吧。并發(fā)很好,但并不一定會(huì)實(shí)現(xiàn)并行。并行是在多核CPU 上同一時(shí)間運(yùn)行多個(gè)任務(wù)或者一個(gè)任務(wù)分為多塊同時(shí)執(zhí)行(如ForkJoin)。單核CPU 的話,就不要考慮并行了。
補(bǔ)充一點(diǎn),實(shí)際上多線程就意味著并發(fā),但是并行只發(fā)生在這些線程在同一時(shí)間調(diào)度、分配到不同CPU 上執(zhí)行的情況下。也就是說,并行是并發(fā)的一種特定形式。一個(gè)任務(wù)里往往會(huì)產(chǎn)生很多元素,這些元素在不參與操作的情況下大都只能處于當(dāng)前線程中,這時(shí)我們可以對(duì)其進(jìn)行ForkJoin,但這對(duì)很多程序員來講有時(shí)候很不好操作、控制,上手難度有些大。這時(shí)如果用響應(yīng)式編程,就可以簡(jiǎn)單地通過所提供的調(diào)度API 輕松做到事件元素的下發(fā)、分配,其內(nèi)部會(huì)將每個(gè)元素包裝成一個(gè)任務(wù)并提交到線程池中,我們可以根據(jù)任務(wù)是計(jì)算型的還是I/O 型的來選擇相應(yīng)的線程池。
在這里,需要強(qiáng)調(diào)一下,線程只是一個(gè)對(duì)象,不要把它想象成CPU 中的某一個(gè)執(zhí)行核心,這是很多人都在犯的錯(cuò),CPU 時(shí)間片會(huì)切換執(zhí)行這些線程。
02
如何理解響應(yīng)式編程中的背壓
背壓,由Back Pressure 翻譯得到,從英文字面意思講,稱之為回壓可能更合適。首先解釋一下回壓,它就好比用吸管喝飲料,將吸管內(nèi)的氣體吸掉,吸管內(nèi)形成低壓,進(jìn)而形成飲料至吸管方向的吸力,此吸力將飲料吸進(jìn)人嘴里。我們常說人往高處走,水往低處流,水之所以會(huì)出現(xiàn)這種現(xiàn)象,其實(shí)是重力所致。而現(xiàn)在吸管下方的水上升進(jìn)入人的口中,說明出現(xiàn)了下游指向上游的逆向壓力,而且這個(gè)逆向壓力大于重力,可以稱這種情況為背壓。這是一個(gè)很直觀的詞,向后的、往回的壓力——Back Pressure。
放在程序中,也就是在數(shù)據(jù)流從上游源生產(chǎn)者向下游消費(fèi)者傳輸?shù)倪^程中,若上游源生產(chǎn)速度大于下游消費(fèi)者消費(fèi)速度,那么可以將下游想象成一個(gè)容器,它處理不了這些數(shù)據(jù),然后數(shù)據(jù)就會(huì)從容器中溢出,也就出現(xiàn)了類似于吸管例子中的情況?,F(xiàn)在,我們要做的事情就是為這個(gè)場(chǎng)景提供解決方案,該解決方案被稱為背壓機(jī)制。
為了更好地解決背壓帶來的問題,我們回到現(xiàn)實(shí)中看一個(gè)事物——大壩。在發(fā)洪水期間,下游沒辦法一下子消耗那么多水,大壩此時(shí)的作用就是攔截洪水,并根據(jù)下游的消耗情況酌情排放,也就是說,背壓機(jī)制應(yīng)該放在連接元素生產(chǎn)者和消費(fèi)者的地方,即它是生產(chǎn)者和消費(fèi)者的銜接者。然后,根據(jù)上面對(duì)大壩的描述,背壓機(jī)制應(yīng)該具有承載元素的能力,也就是它必須是一個(gè)容器,而且其存儲(chǔ)與下發(fā)的元素應(yīng)該有先后順序,那么這里使用隊(duì)列是最適合的了。背壓機(jī)制僅起承載作用是不夠的,正因?yàn)樯嫌芜M(jìn)行了承壓,所以下游可以按需請(qǐng)求元素,也可以在中間根據(jù)實(shí)際情況進(jìn)行限流,以此上下游共同實(shí)現(xiàn)了背壓機(jī)制。在本書后續(xù)內(nèi)容及相關(guān)的配套視頻中會(huì)介紹背壓的相關(guān)API。
03
Reactor 與RxJava 的對(duì)比
關(guān)于響應(yīng)式編程,我寫的《Java 編程方法論:響應(yīng)式RxJava 與代碼設(shè)計(jì)實(shí)戰(zhàn)》一書已經(jīng)出版,那么Reactor 與RxJava 又有什么區(qū)別呢?首先我要明確地告訴你,如果你使用的是Java 8+,那么推薦使用Reactor 3,而如果你使用的還是Java 6+或函數(shù)需要做異常檢查,那么推薦使用RxJava 2。
從上圖可以看到,RxJava 2 和Reactor 共用了一套接口API 標(biāo)準(zhǔn)Reactive Streams Commons,這也說明它們的最終目的是一致的,而且API 具有通用性,這樣也降低了學(xué)習(xí)成本。
下面再來回顧一下RxJava。
迄今為止,RxJava 發(fā)行版主要分三大版本RxJava 3、RxJava 2 和RxJava 1。與RxJava 1 不同,RxJava 3、RxJava 2 直接通過新添加的Flowable 類型來實(shí)現(xiàn)Publisher 的接口定義(RxJava 3 與RxJava 2 并沒有太多區(qū)別,故這里只介紹RxJava 2)。同時(shí),RxJava 2 依然保留了RxJava 1 中的Observable、Completable 和Single,并引入了支持Optional 的Single 升級(jí)版——Maybe 類型。RxJava 1 中的Observable 不支持RxJava 2 中的背壓機(jī)制,背壓機(jī)制是Flowable 的專有功能,不過Observable 內(nèi)部提供了可轉(zhuǎn)換API。需要注意的是,Observable 實(shí)現(xiàn)的是RxJava 2 中自定義的ObservableSource 接口。
在Reactor 中,可以發(fā)現(xiàn)Mono 和Flux 兩種類型都實(shí)現(xiàn)了Publisher 接口,同時(shí)兩者皆實(shí)現(xiàn)了背壓機(jī)制。Flux 可以對(duì)標(biāo)RxJava 2 中的Flowable 類型,而Mono 可以被理解為RxJava 2 中對(duì)Single 的背壓加強(qiáng)版。后續(xù),我們會(huì)進(jìn)行更深入的講解。
同樣,下面再來了解一下Reactor 與RxJava 的不同之處。
- 為了兼容 Java 1.6+ ,RxJava 不得不自行定義了一些函數(shù)式接口,可以參考io.reactivex.functions 下的接口定義。而Reactor 3 則是基于JDK 中提供的java.util.function 來設(shè)計(jì)實(shí)現(xiàn)的。
- 可以很輕松地從java.util.stream.Stream 轉(zhuǎn)換為Flux,也可以很輕松地由后者轉(zhuǎn)換為前者。
- 同樣,可以很輕松地實(shí)現(xiàn)CompletableFuture 與Mono 之間的互相轉(zhuǎn)換,也可以輕松而安全地基于Optional 類型的元素創(chuàng)建Mono。
- Reactor 3 可以更好地服務(wù)于Spring Framework 5,也更適應(yīng)最新版本的JDK。
最后,我們?cè)俸?jiǎn)單介紹一下上圖中的幾個(gè)部分。
Core 是我們主要研究的庫(kù),是Reactor 的核心實(shí)現(xiàn)庫(kù)。其作用與RxJava 2 的核心實(shí)現(xiàn)的作用是一樣的,本書主要介紹reactor-core 模塊。
IPC 可以認(rèn)為它是針對(duì)encode、decode、send(unicast、multicast 或request/response )及服務(wù)連接而設(shè)計(jì)的支持背壓的組件。IPC 支持Kafka、Netty 及Aeron。
Addons 其中包括reactor-adapter、reactor-logback 和reactor-extra。reactor-adapter 可以說是連接RxJava 1/2 中Observable、Completable、Flowable、Single、Maybe、Scheduler 的橋梁,可以方便地與Reactor 3 進(jìn)行轉(zhuǎn)換操作。同樣,這個(gè)庫(kù)對(duì)于Swing/SWT Scheduler、Akka Scheduler 也做了針對(duì)性適配。reactor-logback 用于支持Reactor Core 異步處理Logback 方面的功能。reactor-extra 為數(shù)字類型的Flux 源提供了很多數(shù)學(xué)運(yùn)算的操作。
Reactive Streams Commons 是RxJava 2 和Reactor 共用的一套接口API 標(biāo)準(zhǔn)。