阿里淘宝一直在推的响应式编程到底是个什么鬼?
隨著這些年智能手機的發展和普及,我們的服務器端要應對日益增長的巨大流量。
從開發的角度來看,這就要求我們必須設計出高擴展性和高可用性的程序,以確保能夠適應日益增長的請求所帶來的壓力。
而從使用者的角度來看,他們并不會關心后臺到底是怎樣的,而更關注App界面的易用性和是否美觀,最重要的是每次操作能否得到快速的響應。
舉一個例子,在CPU還是單核的年代,計算機上只運行一個程序就能夠將CPU占用得滿滿當當。如果計算機上同時運行兩個程序,用戶就會覺得卡頓,此時CPU要通過上下文切換輪流處理這兩個程序,而這個切換過程會消耗CPU資源,以及占用更多的內存資源。而在CPU是多核的時代,我們可以同時打開多個程序,還可以隨意切換,并且沒有絲毫停滯感。
與硬件端的發展相似,現在Web前端已經可以實現單頁應用。舉一個例子,在使用音樂App的場景下,點開一個歌單,這個獲取歌單的過程可能由于多種原因而產生延時;與此同時,可以切換到這個App的評論區,查看評論將絲毫不影響后臺正在進行的獲取歌單的任務,等切換回來時可以立馬展示獲取到的歌單。
于是,對開發人員來說,有兩種選擇擺在面前。第一種是在從數據庫中查找歌單任務完成之前,讓用戶一直等待,而不能進行其他操作;第二種是用戶可以在查找歌單任務執行期間同時使用其他功能,而不是切換回來之后重新查找。
對于用戶來說,第二種肯定是最佳選擇,至此引出了我們的話題:基于異步的開發模式和傳統的基于同步的開發模式。后者比較簡單,不必多說,因此我們接下來要介紹的就是基于異步的開發模式,即異步編程模式。
異步編程模式
大家學Java這么久,應該都很清楚入口函數main,異步編程模式意味著在執行main函數的主線程下同時并行且非阻塞地運行一個或多個任務。
這種異步編程模式帶給我們的最主要的好處就是,可使程序的性能和響應速度得到大幅提升。這也是我們一直所追求的目標,無論作為開發者還是消費者,我們都不希望一直處于等待、等待、再等待的狀態。所以通過異步編程模式,消費者可以在明知道某個任務會消耗很長時間的情況下,通過無障礙地使用其他功能,仍能獲得良好的體驗。
并發
可以這么說,并發很好地利用了CPU時間片的特性,也就是操作系統在當前時間片內選擇并運行一個任務,接著在下一個時間片內選擇并運行另一個任務,并把前一個任務設置成等待狀態。其實這里想表達的是,并發并不意味著并行。
具體介紹幾種情況,分別如下。
-
有時候多線程執行會提高應用程序的性能,而有時候反而會降低應用程序的性能。這在JDK中Stream API的使用上體現得很明顯。如果任務量很小,而我們又使用了并行流,反而降低了性能。
-
我們在多線程編程中可能會同時開啟或者關閉多個線程,這會產生大量的性能開銷,也降低了程序性能。
-
當我們的線程同時都處于等待I/O的過程中時,并發可能會阻塞CPU資源,其造成的后果不僅是用戶等待結果,而且會浪費CPU的計算資源。
-
如果幾個線程共享了一個數據,情況就變得有些復雜了,我們需要考慮數據在各個線程中狀態的一致性。為了達到這個目的,我們很可能會使用Synchronized或者lock。
現在,你應該對并發有一定的認知了吧。并發確實是一個好東西,但并不一定會實現并行。并行指的是在多個CPU核上同一時間運行多個任務,或者一個任務分為多塊執行(如ForkJoin)。在單核CPU上就不要考慮并行了。
補充一點,實際上,多線程就意味著并發,但是并行只發生在將這些線程于同一時間調度分配到不同CPU核上執行的時候。也就是說,并行是并發的一種特定形式。
并行開發初探
現實中,再大的項目落實到細節上其實都是由多人協作完成的,而每個人在做自己的工作的時候只能一步步地做。可以說項目會被分解成一個個模塊,由幾個部門一同開發,而在每個部門中其又會被分成一個個子任務,由每個人來負責,最后協作匯總。
我們都知道,CPU也是一個接著一個指令地執行任務的。根據上面描述的場景,我們可以做類比,這里所謂的并行開發就是將一個大任務拆成許多部分來同時執行的。這樣就更好地利用了多處理器多核環境,而拆分的每個任務都是獨立執行的,并且每個任務彼此之間的執行順序也沒什么關系。通過并行執行,大型問題的解決速度直接快了很多,同時也可以更加高效地利用內存。
基于線程模型,我們可以通過良好的控制來實現用戶訪問功能的流程,但是為了后臺服務器能夠更好地進行數據的處理,可以根據需要將并發處理設定為一個開關操作。此操作可自動根據任務的數量來分配線程,同時我們需要一套處理數據的模型。從架構層面,我們拿消息中間件來說,消息中間件連接的各個工程作為一個整體就可以看成數據的一種流向處理,客戶端通過訂閱生產所設定的Topic來做數據的接收處理。而往小的API方向考慮,可以試著理解JDK中流的概念或者Linux命令中管道的概念(只不過管道和流都是基于推的模式),從中抽象出一個更好用的模型,然后將并發處理的開關操作作為其中的一環加入即可。這時,響應式編程(Reactive Programming)就呼之欲出了。
流(Stream)
因為本書后面會多次使用流的概念,所以在此專門介紹一下。在我們平時的編程開發中,往往練就了一番使用Collections框架提供的API熟練、高效地處理數據的能力,對于較復雜的數據,我們往往會用循環來檢查。為此,Collections API針對循環遍歷的上手難度和方便性做的優化最多,但此優化只適用于從上到下依次執行,并不能高效地利用我們的多核CPU,而通過流則可以很好地解決這個問題。
其實對于流,完全不需要將其想得多么高大上,其就像一條河中的水,有源頭,有處理過程,同樣有最好的消費歸宿。“滾滾長江東逝水”這句歌詞恰好道出了流的一次性特性。
通過上面的簡單場景,可以總結流的特性如下。
-
流中的元素是有順序的。
-
流需要一個數據源。流可以將集合、數組、文件或者其他的I/O資源作為其輸入的數據源。
-
豐富而流暢的處理操作API。流API提供了與其他函數式語言相同的API(具體API的名字都一樣,基本沒有什么學習難度),這方便了統一操作,如filter、map、skip、limit等操作。
-
包含一些隱式操作。比如,針對元素迭代,我們無須手動指定,這是默認實現的。另外,在最終具體消費的時候,才會執行流的整個處理過程。
響應式流(Reactive Stream)
從消息中間件的使用場景可知,在這個數據爆炸的時代,我們的需求在升級,不僅是前臺訪問后臺,取一組數據;而且更多的是面對海量數據,這時需要將其過濾、修改、轉換為我們所需要的數據,而處理這些數據需要大量的時間。為了突破這個瓶頸,我們可以使用流來快速處理和響應。也正是基于此,下面來學習一套類似于消息中間件整體使用場景的API,為此需要了解響應式流API的規范和設計思想,以更好地服務業務,而無須苦苦掙扎,使用最基礎的流API。
何為消息中間件,從簡單的場景理解它就像QQ。比如,我們都加入了QQ群,而作為群中的一員,我們既是消息的生產者,也是消息的消費者。群中的成員想要獲取消息,首先要加入這個群,即訂閱,于是生產、消費、訂閱這幾個動作就都產生了。
下面介紹一下響應式流的特性。
異步
前面已經有所介紹,在傳統開發模式下,我們是一個方法接著一個方法執行的,最終消耗的時長是這些方法消耗的總時長。而基于異步編程模式,由于這些操作是同時并行執行的,因此最終消耗的時長是這些操作中消耗時間最長的操作所消耗的時長。也就是說,在大量任務需要執行的情況下,異步編程模式為我們的程序帶來了快速響應。
背壓(Back Pressure)
背壓是響應式流的一個規范。比如,上游滔滔江水來,下游河道窄淺,一旦發生洪水,后果將不堪設想,為了減輕下游的壓力,索性就建設大壩。回到響應式流,流中的元素會由生產者(Producer)在一端生產出來,而在另一端由消費者(Consumer)消費掉。一旦元素的生產速度超過了消費者的消費速度,就會造成產品的積壓,即元素的積壓。隨著這種積壓的不斷增加,程序性能就會下降,直至程序“掛掉”。背壓(Back Pressure)就是用來解決這個問題的,雖然它可能會增加元素的處理時間,但是它建立起了一個彈性機制,允許程序內部按需調節而不至于使程序崩潰。
具體來說就是,元素由發布者生產、發布,由訂閱者或消費者在下游收集。接下來,消費者會根據需求發送一個信號給上游,以此來保證可以將所需元素安全地推送給消費者,而發送信號這個動作是異步進行的。對于訂閱者來講,其可以通過一個拉取策略來發送更多的請求以獲取元素。
響應式開發的設計原則
從異步的角度可以知道,我們往往會將一個任務拆分成許多小任務,各個小任務之間可以互不阻塞地異步執行(分而治之),在每一個任務都完成后,將它們的結果進行組合并生成一個結果流。我們通過響應式編程設計即可很輕松地做到這一點。下面介紹一下響應式開發應該遵循的一些設計原則。
響應式系統提供了諸如可響應能力、高可用性、彈性機制、消息驅動和可擴展性等機制,從而確保響應式編程不會使系統資源一直被占用,這樣可以使系統的其他組件正常運行。
可響應能力
可響應能力是一個應用程序最重要的功能,在編程的時候我們希望能有一個高效、統一的格式,包括對錯誤的處理響應,這一點在Web開發過程中已經很常見了。
高可用性
我們的服務器可能會掛掉,無法提供服務。這時可以提供一個備用服務器,這樣在主服務器宕機的時候,備用服務器能夠頂替上來,達到服務不間斷的效果。于是我們看到日常開發中經常會提到各式各樣的集群,集群的一個目的就是實現服務不間斷。同樣,應用程序可能會被拆分為很多系統模塊并做成微服務,微服務之間彼此隔離。如果一個微服務掛掉,并不會影響整個系統提供服務,可將損失最小化。
彈性機制
每當有數據到達時,系統會根據需要分配計算資源,以保證數據得到及時處理。響應式系統提供了一個彈性算法機制,當資源需求增長時,分配的計算資源也隨之增長;當資源需求減少時,多分配的資源也會隨之回收,避免浪費。
消息驅動
簡單點講,消息驅動就是將每個人比作系統中的一個個微服務組件,人與人之間的交流通過消息進行。從這個場景來看,響應式系統通過使用異步的消息在各個組件間交流、通信,這樣就可以成功地做到各個組件之間的隔離和松耦合,也就可以更輕松、靈活地擴展和維護系統了。
可擴展性
開發中經常會面對持續不斷出現的新需求。可以這樣說,我們現在開發的軟件無法完全滿足未來的需求,不僅可能會因為需要添加新功能而無法滿足,而且可能會因為日益增長的訪問壓力而逐漸無法滿足,并且這個壓力可能僅僅集中施加在幾個模塊上。在理解了這些之后,我們要做的不僅是在一個項目中添加代碼,而且還要在不影響舊代碼的前提下,對這個項目進行擴展。部分功能模塊可以進行水平擴展(集群化),也可以單獨開發微服務進行云平臺掛載(即功能性的垂直擴展),而路由和相關的其他模塊只需要修改極少的代碼。
響應式開發的好處
響應式開發的好處主要包含以下幾點。
-
提高所開發程序的性能。
-
在多核機器上,提高了計算資源的利用率。
-
為異步編程提供了一個更靠譜的可維護方案。
-
提供了背壓機制,也就是對計算資源提供了過載保護功能。
響應式開發工具庫
已經有很多工具庫實現了響應式流的標準,包括Akka、Reactor、RxJava、Streams、Vert.x等。下面簡單介紹幾種,在后面的章節中我會重點講解RxJava(關于Reactor,會在本系列叢書的另一本書中具體講解)。
RxJava簡介
通過官方GitHub可知,RxJava是使用Java語言開發的專門針對JVM的一種響應式擴展工具,通過它可以輕松地在服務器端實現并發操作。RxJava的目的就是處理客戶端越來越復雜的請求,在服務器端通過并行計算快速地響應請求。
接下來,我們開始了解RxJava到底是怎么一回事,作為數據的消費者,我們會對獲得的數據做出各種反應。有句話說得好,“跳出三界外,不在五行中”,在我們以旁觀者的視角來看這件事的時候,其實它就是一個觀察者模式的實際體現。RxJava下的響應式編程其實就是基于影院里的電影(Observable)提供的內容(生產者數據)傳播給訂閱者,然后訂閱者做出相應的反應。
結合上面的場景,下面對RxJava所涉及的要點進行解釋。
-
Observable:表示數據源,Observable會發出一定數量的元素,發送可能會成功,也可能在這個過程中出現狀況而失敗。從電影院的場景可以知道,同一時間Observable可以有多個訂閱者。
-
Observer或Subscriber:表示訂閱者,通過監聽Observable來消費Observable所發送的元素。
-
Methods:表示一系列操作API,對下發數據進行加工整合。
-
onNext:在一個元素被Observable發送出去的時候,通過該方法可以調用每一個訂閱者。
-
onComplete:在Observable成功發送完所有數據后,會調用這個方法來收尾。
-
onError:當Observable發送數據的過程中出現錯誤的狀況時,會調用這個方法結束發送并返回一個error事件。
-
RxJava所帶來的好處主要如下。
-
允許我們進行一系列的異步操作。
-
有時為了跟蹤狀態,我們會通過一個原子類變量保存之前計算的值。我們無須專門使用原子類來跟蹤狀態,因為RxJava中已經封裝了這些操作。
-
RxJava提供了一個在整個執行過程中發生錯誤時的處理途徑。
Reactor簡介
Spring 5官方文檔提到,其通過Reactor的支持,在服務器端獲得了更高的性能和更快的響應速度。SpringWebFlux作為新一代的Web開發框架,以Reactor作為基礎框架進行異步編程的開發,從而可以使我們寫出性能更好的Web應用程序。如果大家看過我博客中關于Spring 5源碼分析的系列文章,就可以知道Spring MVC框架的整個運行過程其實使用了事件驅動(Event-Driven)模式,而Reactor自身的設計也使用了這個模式。參考前面的電影院場景,當電影的畫面和聲音傳到你的眼睛和耳朵中時,會引發你喜怒哀樂的情緒反應。再形象一點,在拳擊選手一個直拳要打到對方選手臉上的時候,對方選手會躲閃。電影的畫面和拳擊選手的直拳動作都是一種事件的表現,根據事件做出相應處理的整個過程就是所謂的事件驅動。在Spring里,這就相當于我們的后臺服務器接收事件請求,通過multicaster多路分發器來分發給相應的監聽器(Listener),最后由監聽器里定義的相應的handler來做具體處理。整個過程大概就是這樣的。
知道了上面的這些內容,通過使用Reactor,我們寫出的程序就可以按照事件驅動的模式很輕易地異步運行了。
1.Reactor的優點
Reactor支持完全無阻塞,其主要的目標之一就是解決傳統Web開發方案對于異步支持的各種弊病。它提供了十分有效的途徑來支持背壓。它還有以下優點。
-
豐富的API,可以對數據流進行操作。
-
提供了一種可讀性更強的代碼書寫方式,使我們所寫的代碼可以更方便地得到維護。
-
與流相同,無消費,不執行。
-
消費者具備發信號通知生產者元素按需下發的能力(RxJava同樣具備)。
2.Reactor的核心功能
Reactor項目的主要模塊是reactor-core,這是一個專門用于支持響應式流規范的類庫,其支持Java 8及后續版本。通過查看Reactor API,可知它和RxJava很像。Reactor3是Reactor?2和RxJava的核心貢獻開發者一起完成的一個混合版本,這也是本章把這幾個東西放在一起介紹,然后分章講解的很重要的原因,因為這樣更易于理解。
Reactor與RxJava有相同的Publisher、Subscriber、Subscription和Processor核心接口。這里只簡單介紹Publisher最常用的兩個實現Mono和Flux,以及相關的操作符。
-
Mono:表示一個特殊的Publisher,它可以發送0個或1個元素。
-
Flux:表示一個特殊的Publisher,它可以發送0到n個元素。
-
操作符:元素在從Publisher發送給訂閱者之前,可能會需要進行一些處理,包括轉換、過濾操作等。
MongoDB簡介
在后面的實戰開發中,我們可能會用到MongoDB,官方提供的MongoDBReactive Streams Java版本的驅動包API可以對MongoDB進行異步流處理,而且是無阻塞支持背壓的。這里只是提一下,說明我們的數據庫操作層面也開始做到了對響應式流API標準的支持。
響應式項目用例
前面說了那么多,大家可能依然有點不明白,那么為了更好地理解響應式系統(Reactive System),我們看看它與傳統項目的不同之處。
以我們生活中的股票場景為例,我們需要看到股票信息的實時動態展示。這時我們會打開并保持一個頁面,這個頁面可以實時顯示股票信息。開發人員需要做的是,將最新的數據更新到這個股票展示頁面上。作為股民,面對的是“差之毫厘,失之千里”的局面。對他們來說,數據刷新得越及時,對決策越有利(在這里,我們只從響應式的角度來考慮這個問題,現實項目中會有基于WebSocket的實現,Spring MVC中也有SSE的實現)。
傳統開發模式
根據以往的開發經驗,我們會主動地檢查股票價格有沒有變化,如果有變化,就從后臺拉取最新的數據。如圖1-1所示的流程圖就代表著傳統開發模式。? ? ? ? ? ? ? ? ? ? ? ? ? ??
在傳統開發模式下,一旦開始渲染訪問頁面,就會每隔一段時間(圖1-1中是1分鐘)發送AJAX請求到后臺的查詢服務去請求股票價格數據。使用這種方式,無論股票價格是否真的發生變化,都會去請求,但無法保證股票價格的變化會被立刻傳遞到Web頁面上。
響應式開發模式
響應式開發模式通過事件驅動的方式將各個組件連接到一起,以實現在事件發生時其他組件可以立即進行響應。
也就是說,在加載股票價格頁面后,這個頁面會有一個專屬ID注冊到股票查詢服務上。一旦使股票價格發生變化的事件產生,這個事件(Event)就會觸發響應,最新的股票價格就會在Web頁面上進行更新顯示。如圖1-2所示說明了整個流程。
可以看到,響應式開發模式一般包括下面3個步驟。
(1)訂閱(Subscribing)事件。
(2)事件的發生與傳播。
(3)解除訂閱。
在股票價格頁面初始化加載的時候,其中有一個動作就是訂閱當前使股票價格發生變化的事件源,可以認為事件源是消息中間件里的主題(Topic)(或者是我們訂閱的一個RSS主題),而不同的響應式框架會有不同的具體方式,使用消息中間件也可以實現訂閱。
在我們所關注的某只股票價格發生變化的時候,一個新的事件就會產生并分發給這個事件的訂閱者。我們的Web頁面會及時地接收并更新股票價格數據。而一旦Web頁面關閉或者刷新,一個解除訂閱的請求就會被發送至后臺。
傳統開發模式和響應式開發模式的比較
可以看到,傳統開發模式是比較簡單的,而響應式開發模式需要我們實現一個訂閱和事件傳播鏈。如果事件的傳播需要跨項目,也就是涉及其他項目,那么就可能會使用到消息中間件,這將會變得復雜,其并不屬于本書的范圍,此處不做討論。
在傳統開發模式中,更新股票價格頁面主要是基于盲目的主動拉取來實現的,前端根本就不知道會不會有數據發生變化。這也就意味著無論后臺數據有沒有發生變化,前端都需要定時從后臺拉取一次數據。而在響應式開發模式中,一旦注冊訂閱了價格變動事件,那么只有這只股票的價格發生變化才會觸發一系列的操作,這樣明顯提高了程序性能和用戶體驗。
在傳統開發模式中,這個例子中線程的生命周期會比較長,這也就意味著該線程所使用的資源在這個過程中會被線程鎖定。考慮到同一時刻服務器會接收大量的請求,這樣勢必會造成更多的線程相互爭奪資源。在響應式開發模式中,線程生存的時間短,這也就意味著爭奪資源的情況較少。
總結
以上是生活随笔為你收集整理的阿里淘宝一直在推的响应式编程到底是个什么鬼?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 来自十年互联网人的大厂等级晋升攻略
- 下一篇: 低效能人士的七个习惯