徹底搞懂響應式編程
我們知道,系統面對大流量、高并發的訪問請求時,就可能會出現一系列性能問題,導致服務喪失了即時的響應性。如何時刻確保系統具有應對請求壓力的能力,是架構設計的核心問題之一。
經典的服務隔離、限流、降級以及熔斷等機制能夠在一定程度上確保系統的響應性。但這些機制更多的是從系統架構和應用部署的角度出發解決問題,而不是編程技術本身。今天我們要介紹的是構建系統響應性的一種嶄新的解決方案,這就是響應式編程(Reactive Programming)。
我們知道,傳統的編程模型采用的是同步阻塞式(Blocking)的請求響應過程,這是現有各種經典解決方案所不得不面對的一種限制。 而響應式編程打破了這種限制,采用了異步非阻塞式(Non-Blocking)的編程模型,從而提高服務的響應能力。
這里提到了同步阻塞和異步非阻塞這兩個核心概念,正確理解這兩個概念是你掌握響應式編程的前提條件。所以接下來,我們就來看看響應式編程技術是如何基于它們誕生的。
為什么需要響應式編程?
如果你使用 Spring 框架開發過 Web 應用程序,那么你一定對下面這種開發方式非常熟悉:
public Order getRemoteOrderByOrderNumber(String orderNumber) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Order> result= restTemplate.exchange(
"http://orderservice/orders/{orderNumber}", HttpMethod.GET, null, Order.class, orderNumber);
Order order= result.getBody();
processOrder(order);
return order;
}
這是一個查詢訂單(Order)信息的應用場景,我們使用了 Spring 中的 RestTemplate 模板工具類,通過該類所提供的 exchange() 方法對遠程 Web 服務所暴露的 HTTP 端點發起了請求。
這種實現方式在日常開發中非常有代表性, 基于 Spring Cloud 開發的微服務系統,本質上,也是通過這種方式完成服務與服務之間的遠程調用。但是,這個方法實際上存在明顯的缺陷,因為處理過程是阻塞式的。
正是因為同步阻塞的存在才導致了異步非阻塞相關技術的誕生和發展,進而才有了今天要介紹的響應式編程技術。那么,究竟什么是阻塞式呢?
同步阻塞
我們首先來分析代碼中的線程模型,看看問題出在哪里。為了更好的分析整個調用過程,我們假設服務的提供者為服務 A,而服務的消費者為服務 B,那么這兩個服務的交互過程應該是這樣的。
服務 A 和服務 B 的交互過程圖
可以看到,當服務 B 向服務 A 發送 HTTP 請求時,線程 B,只在發起請求和響應結果的一小部分時間內有效使用 CPU,而更多時間則是在阻塞式地等待來自服務 A 中線程的處理結果。顯然,整個過程的 CPU 利用效率是很低的,很多時間被浪費在了 I/O 阻塞上,無法執行其他處理過程。
更進一步,我們繼續分析服務 A 中的處理過程。
如果我們采用典型的三層架構,那么沿著 Web 服務層->業務邏輯層->數據訪問層整個調用鏈路,每一步的操作過程都存在著前面描述的線程等待問題。也就是說,整個技術棧中的每一個環節都可能是同步阻塞的。
這樣的話,整個調用鏈路的資源利用率都會變低,導致請求的處理過程出現延遲,而喪失了我們想要的即時響應性。
Web 應用程序三層架構
異步非阻塞
為了解決同步阻塞問題,可以引入異步非阻塞的相關技術。異步非阻塞技術能夠通過多線程技術,將整個請求處理過程交由不同線程并行處理,提高了系統資源利用率。
在 Java 世界中,一般會采用回調(Callback)和 Future 這兩種機制,但這兩種機制都存在一定局限性:
回調的核心問題在于,處理過程會形成一種嵌套結構,給代碼的開發和調試帶來很大的挑戰。
Future 機制本質上是一種多線程技術,大量線程之間的相互協作需要頻繁進行上下文切換,同樣會導致資源利用效率低下。
其實引入響應式編程技術,我們就可以很好地解決這種類型的問題。
響應式編程采用全新的響應式數據流(Stream),實現異步非阻塞式的網絡通信和數據訪問機制,能夠減低不必要的線程等待時間。那么,所謂的響應式編程到底是什么樣子的呢?
什么是響應式編程?
響應式編程技術的核心是數據流,而數據流又是構建在傳統的事件驅動架構與發布訂閱模式之上。在講解響應式編程技術之前,我們先來看一下發布訂閱模式和事件處理相關的技術體系。
發布訂閱模式和事件處理
相信你應該對設計模式中經典的觀察者模式不陌生。觀察者模式擁有一個主題(Subject)以及針對這個主題的一個依賴者列表,這些依賴者被稱為觀察者(Observer)。
而發布訂閱(Publish-Subscribe)模式可以認為是對觀察者模式的一種改進。在這種模式中,發布者和訂閱者相互之間可以沒有直接的依賴關系,而是通過發送事件到事件處理平臺上完成整合。
針對開頭提到的訂單查詢操作,我們可以基于發布訂閱模式重構流程。通過構建發布訂閱模式以及事件處理平臺,我們具備了傳播和處理異步事件的能力,從而為實現響應式編程提供了基礎。(圖 3)
發布 - 訂閱模式下的訂單信息獲取過程
再來看單個服務的內部,在三層架構中整個調用鏈路同樣可以用發布訂閱模式來重構。這時,數據庫中的數據一有變化就會通知到上游組件,而不是上游組件通過主動拉取的方式獲取數據。這樣做相當于,讓處于調用鏈路中的各個組件由同步調用轉化為了異步調用,圖中的虛線和箭頭方向表達了這層含義。
基于響應式實現方法的數據流轉時序圖
數據流和響應式
顯然,上圖中異步事件傳播的思想可以擴展到整個系統。
你可以想象系統中會存在著很多類似 OderEvent 這樣的事件。每一種事件會被用戶的操作或者系統自身的行為觸發,并形成了事件的集合。我們可以把這個集合看成是一串串連起來的數據流,而系統的響應能力就體現在對這些數據流的即時響應過程上。
全流程數據流示意圖
對于技術實現過程而言,數據流是一個全流程的概念。也就是說,無論是底層數據庫、服務層、Web 服務層,或是在這個流程中所包含的任意中間層組件,整個數據傳遞鏈路都應該采用事件驅動的方式來運作。這樣,我們就可以不用傳統的同步調用的方式來處理數據,而是由處于全流程中的各層組件自行執行事件,實現了全流程的異步非阻塞處理機制。這就是響應式編程的核心特點。
相較傳統開發普遍采用的“拉”模式,在響應式編程下,基于事件的觸發和訂閱機制,這就形成了一種類似“推”的工作方式。
推模式下的數據流處理方式示意圖
這種工作方式的優勢就在于,生成事件和消費事件的過程是異步執行的,所以線程的生命周期都很短,也就意味著資源之間的競爭關系較少,服務器的響應能力也就越高。這就是響應式編程的精髓,也是解決系統性能問題的關鍵所在。
講到這里,你可能會問,我們如何來使用響應式編程技術來開發業務系統呢?不用擔心,到目前為止,業界已經誕生了諸如 RxJava、Project Reactor、Akka 等一大批優秀的響應式編程框架。
而在 Spring 5 中,也引入了 WebFlux、Reactive Spring Data 等新一代的編程組件來實現響應式 Web 服務和響應式數據訪問。這種框架和工具,可以很好的解決傳統同步阻塞式處理方式所存在的性能問題。
總結
今天我們系統分析了傳統服務調用存在的問題,從而引出響應式編程概念和實現方法。
從技術演進的過程和趨勢而言,響應式編程的出現有其必然性。
但是響應式編程也不是一種完全顛覆式的技術體系,而是在現有的異步調用、觀察者模式、發布訂閱模式等的基礎上發展起來的一種全新的編程模式,能夠給系統帶來即時響應性的優點。