1. 什么是響應式編程
在計算機中,響應式編程或反應式編程(英語:Reactive programming)是一種面向數據流和變化傳播的編程范式。這意味着可以在編程語言中很方便地表達靜態或動態的數據流,而相關的計算模型會自動將變化的值通過數據流進行傳播。
例如,在命令式編程環境中,a=b+c 表示將表達式的結果賦給 a,而之后改變 b 或 c 的值不會影響 a 。但在響應式編程中,a 的值會隨着 b 或 c 的更新而更新。
響應式編程是基於異步和事件驅動的非阻塞程序,只需要在程序內啟動少量線程擴展,而不是水平通過集群擴展。
設想一個場景,從底層數據庫驅動,經過持久層、服務層、MVC層中的model,到用戶的前端界面的元素,全部都采用聲明式的編程范式,從而搭建一條能夠傳遞變化的管道,這樣我們只要更新一下數據庫中的數據,用戶的界面上就相應的發生變化,從而無需前端輪詢才能獲取到最新的數據。
簡單來講,我們以前寫的程序是阻塞式的,當一個請求任務過來時,線程會阻塞,等到這個任務完成后再返回出去。而響應式編程則是一個請求任務過來時,會有其他的線程去做處理,當任務執行結束后再異步的通知回去。
2. 為什么要使用響應式編程
在如今互聯網時代的大背景下,Web應用通常要面對高並發、海量數據的挑戰,性能從來都是必須要考量的核心因素。
阻塞便是性能殺手之一。
多數人不認為阻塞是一個比較大的問題,至少覺得除了網絡I/O之外,讀寫文件和數據庫還是很快的,許多人也一直在寫阻塞的代碼。
那么 I/O 操作具體有多慢?
2.1 CPU眼中的時間
以下內容來源 https://blog.csdn.net/get_set/article/details/79466402
CPU絕對稱得上是“閃電俠”,因為它們做事都有自己的一套時鍾。我們故事的主人公是一個主頻為2.5GHz的CPU,如果它的世界也有“秒”的概念,並且它的時鍾跳一下為一秒,那么在CPU(CPU的一個核心)眼中的時間概念是啥樣的呢?
CPU先生所在的組是硬件部計算組。對它來說,與其一起緊密合作的幾個小伙伴還能跟的上它的節奏:
- CPU先生很利索,只需要一秒就可以完成一個指令,復雜的動作可能需要多個指令。
- 好在“貼身秘書”一級緩存反應比較快,能夠秒懂CPU先生的意思。
- 來自“秘書組”的二級緩存雖然要十幾秒才能“get”到CPU先生的點,但也不算太遲鈍。
- 和內存組的合作已經習以為常了,跟內存請求的數據通常要4-5分鍾才能找到(內存尋址),不過也還好啦,畢竟一級緩存那里能拿到80%想要的數據,其余的二級緩存也能搞定一大部分,不怎么耽誤事兒。
CPU先生是典型的工作狂,任務多的時候,通宵達旦也毫無怨言,但是有什么事情讓它等,那簡直要他命了。恰恰一起共事的其他組(尤其是I/O組的磁盤和網卡)相對來說那效率是低的離譜啊:
- 關於I/O組的同事,CPU先生已經抱怨很久了,每次找SSD要東西,都要花費4-5天才能找到(尋址),等到數據傳送過來,幾周都過去了。機械磁盤更是過分地離譜,跟他要個數據,竟然要平均花費10個月才能找到,如果要讀取1M的數據,竟然要20個月!這種員工怎么還不下崗?!
- 關於網卡,CPU先生知道它們也盡力了,畢竟萬兆網絡成本頗高。與機房內的其他小伙伴們用千兆網絡互相溝通也算順暢,給另一台機器的CPU朋友發送1K的書信,最快七八個小時就可以送過去了。但是1K的書信經過層層包裹,實際也寫不了多少話。更要命的是,網卡們的溝通手續繁雜,每次網絡溝通前的 “你好能聽到我嗎?——我能聽到,你那邊能聽到我嗎?——我也能聽到你,那我們開始吧!” 這樣的握手確認都要花掉很長的時間,不過不能當面溝通,也只能這樣了。這還好,最恐怖的是與其他城市的小伙伴溝通,有時候傳遞消息要花費好幾年呢!
由此可見,對於CPU先生來說,想要讓工作充實起來實在不容易,不過多虧了內存組的小伙伴幫忙分批緩存往返於I/O組的數據,矛盾才有所緩解。
這個圖只能明顯看出涉及I/O的時間條,我們轉換成對數刻度的圖看一下:
這個圖並不是直觀的比例,橫軸上每個刻度是一個數量級,可見I/O的速度與CPU和內存相比是要差幾個數量級的。由此可見,對於大型高並發場景下的Web應用,緩存有多重要,更高的緩存命中率就意味着性能。
- 並行化:使用更多的線程和硬件資源;
- 異步化:基於現有的資源來提高執行效率。
3. 基礎概念
在介紹主題之前先普及幾個概念:
3.1 Backpressure(背壓)
背壓是一種常用策略,使得發布者擁有無限制的緩沖區存儲元素,用於確保發布者發布元素太快時,不會去壓制訂閱者。
3.2 Reactive Streams(響應式流)
一般由以下組成:
- 發布者:發布元素到訂閱者
- 訂閱者:消費元素
- 訂閱:在發布者中,訂閱被創建時,將與訂閱者共享
- 處理器:發布者與訂閱者之間處理數據
3.3 Mono 和 Flux
- Mono:實現發布者,並返回 0 或 1 個元素
- Flux:實現發布者,並返回 N 個元素
4. Spring Webflux
Spring Boot Webflux 是基於 Reactor 實現的。Spring Boot 2.0 包括一個新的 spring-webflux 模塊。該模塊包含對響應式 HTTP 和 WebSocket 客戶端的支持,以及對 REST,HTML 和 WebSocket 交互等程序的支持。一般來說,Spring MVC 用於同步處理,Spring Webflux 用於異步處理。
Spring Boot Webflux 有兩種編程模型實現,一種類似 Spring MVC 注解方式,另一種是使用其功能性端點方式。
4.1 適用性
一圖就很明確了,WebFlux 和 MVC 有交集。但是注意:
- MVC 能滿足場景的,就不需要更改為 WebFlux。
- 要注意容器的支持,可以看看下面內嵌容器的支持。
- 微服務體系結構,WebFlux 和 MVC 可以混合使用。尤其開發 IO 密集型服務的時候,選擇 WebFlux 去實現。
4.2 內嵌容器
跟 Spring Boot 大框架一樣啟動應用,但 WebFlux 默認是通過 Netty 啟動,並且自動設置了默認端口為 8080。另外還提供了對 Jetty、Undertow 等容器的支持。開發者自行在添加對應的容器 Starter 組件依賴,即可配置並使用對應內嵌容器實例。
但是要注意,必須是 Servlet 3.1+ 容器,如 Tomcat、Jetty;或者非 Servlet 容器,如 Netty 和 Undertow。
4.3 數據庫
支持 reactive 編程的數據庫只有 MongoDB , redis , Cassandra , Couchbase 。
4.4 快速上手
工程依賴
代碼清單:spring-boot-webflux/pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Service 類
代碼清單:spring-boot-webflux/src/main/java/com/springboot/springbootwebflux/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl implements UserSerivice {
private static Map<Long, User> map = new HashMap<>();
static {
map.put(1L, new User(1L, "www.geekdigging.com", 18));
map.put(2L, new User(2L, "極客挖掘機", 28));
}
@Override
public Mono<User> getUserById(Long id) {
return Mono.just(map.get(id));
}
}
Controller 類
代碼清單:spring-boot-webflux/src/main/java/com/springboot/springbootwebflux/controller/UserController.java
@RestController
public class UserController {
@Autowired
UserSerivice userSerivice;
@GetMapping("/getUserById/{id}")
public Mono<User> getUserById(@PathVariable Long id) {
return userSerivice.getUserById(id);
}
}
通過上面的示例可以發現,開發模式和之前 Spring MVC 的模式差別不是很大,只是在方法的返回值上有所區別。
5. 示例代碼
6. 參考
https://blog.csdn.net/get_set/article/details/79466402
http://www.ityouknow.com/springboot/2019/02/12/spring-boot-webflux.html