Reactive聲明式響應式編程到底帶給我們什麼?

前言

希望有reactive庫的使用經驗的小夥伴閱讀,會比較有體會

記得我第一次接觸Reactive是在讀一本知秋大佬寫的RxJava書的時候,書名叫《Java編程方法論:響應式RxJava與代碼設計實戰》,這本書讓我初識了Reactive的底層實現範式,如果有讀過RxJava的源碼就可以知道,其底層類似擴展了面向對象的設計模式 — 觀察者模式,一個事件數據流從生產者發射,一個個消費者(觀察者)將會(觀察到並)響應從上游發射的數據流,並且RxJava提供了大量的操作符可以很靈活方便的管理數據流的變化,稱之爲響應變化即爲響應式編程,當時對Reactive僅僅停留在知道他的技術實現的層面,但並不知道使用Reactive的優勢,或者說Reactive解決了什麼場景下的問題(爲什麼使用Reactive?)。

我曾大量搜索網上博客討論Reactive的優勢、好處,我感覺大部分文章都沒講到精華,這篇文章中我將對Reactive的理解寫下,希望能讓更多人理解爲什麼要使用Reactive,這也是本篇文章的誕生初衷之一。

本篇文章對於Reactive的很多思考都來自於 小馬哥的一篇對於 Reactive Programming 的理解,和 Reactor庫官方文檔 在這裏感謝這些作者。

我認爲有以下場景驅使我們需要使用Reactive:

  • 我們需要異步編程去更有效率的利用系統資源
  • 處理事件流例如NIO可讀可寫事件、鼠標點擊、鍵盤輸入等等,對事件作出一些響應

接下來逐個探討這些議題。

1. 異步編程,效率利用系統資源

1.1 阻塞可能是浪費的

引用自Reactor庫文檔中的 3.1節Blocking Can Be Wasteful:

Usually, Java developers write programs by using blocking code. This practice is fine until there is a performance bottleneck. Then it is time to introduce additional threads, running similar blocking code. But this scaling in resource utilization can quickly introduce contention and concurrency problems.

Worse still, blocking wastes resources. If you look closely, as soon as a program involves some latency (notably I/O, such as a database request or a network call), resources are wasted because threads (possibly many threads) now sit idle, waiting for data.

So the parallelization approach is not a silver bullet. It is necessary to access the full power of the hardware, but it is also complex to reason about and susceptible to resource wasting.

這裏Reactor的大概意思是:

  • 當程序中有一些潛在的IO例如數據庫請求、網絡調用,線程資源由於這段時間需要等待IO數據的到來而被閒置,此時如果請求數量增大,就會遇到一個性能瓶頸(performance bottleneck),爲了及時響應處理別的請求,此時往往需要引入額外的線程,但隨之而來也會引入一些資源競爭和併發問題,Reactor認爲,這裏因爲阻塞而引入額外的這部分線程是被浪費掉的

這裏提到同步阻塞的這部分線程是被浪費掉的,是因爲他什麼事都沒做,卻要停在那邊,佔用了一些內存資源(線程上下文堆棧保存、創建銷燬線程開銷)

來看一個例子,假設A用戶登陸系統的同時,B用戶也同時進來登陸:
在這裏插入圖片描述

通常,我們引入多線程即可解決用戶B的延時響應問題:
在這裏插入圖片描述

在這個互聯網火熱的時代,萬級的併發都是常態,那麼如果一萬個用戶同時進來登陸系統,爲了保證響應性及時,是否開一萬個線程呢?肯定不可能,需要的系統資源將是無止境的,這對硬件要求過高了。那如果我們只使用100個線程循環使用呢(線程池)?那麼同一時間有一萬用戶同時登陸,在前0.8s只會有100個用戶能及時得到響應,後面來的9900用戶都會阻塞0.8s然後繼續有100個用戶能得到響應,以此類推大部分的用戶將很久得不到響應。

這裏的關鍵點就在於idleTime太久了,讓線程白白在那邊乾等着,造成只能引入額外線程才能及時處理別的用戶的請求

其實Reactor暗示了,異步非阻塞將避免浪費這部分線程,那麼異步非阻塞是如何做到的呢?
在這裏插入圖片描述

關鍵點在於非阻塞的請求調用數據到來時的通知,數據到來期間線程是不管的,可以繼續處理別的請求,這樣對比來看,阻塞確實可能是浪費資源的

雖然每個用戶都增長的少許延遲時間(實際場景的耗時和例子其實差別挺大,這裏只不過放大數據,讓大家能更形象看出非阻塞模型的優勢之處),但是在阻塞模型中我們本來需要使用3個線程(3個用戶併發)保證延時及時性,使用了非阻塞模型只需要一個線程即可做到,底層技術實現來說,以Netty框架的NIO非阻塞模型爲例,就與多路複用有關係,大致來說是註冊讀寫事件,潛在的IO操作交給了內核去處理,用戶態的線程就不需要等待,如果是epoll模型,IO結果就會由內核來通知用戶態線程,等待IO期間,用戶態線程自由分配動作處理其他請求。

我們結合Spring5官方文檔的幾句話一起來理解一下:

Reactive and non-blocking generally do not make applications run faster. They can, in some cases, for example if using the WebClient to execute remote calls in parallel. On the whole it requires more work to do things the non-blocking way and that can increase slightly the required processing time.

The key expected benefit of reactive and non-blocking is the ability to scale with a small, fixed number of threads and less memory. That makes applications more resilient under load because they scale in a more predictable way.

  • 非阻塞配合Reactive通常不能使得應用運行更快速,甚至還可能會增加一些處理時間
  • 關鍵點在於Reactive和非阻塞可以使得少而固定的線程和少量的內存消耗,就可以使得應用具有很好的伸縮性

關鍵在於,只需要少量的系統資源,就可以具有很好的伸縮性,這裏大概指的是在併發量激增,連接數達到萬級以上,應用始終能使用少量資源去很好的處理用戶請求。

下面我舉一個我在現實項目中遇到的問題,來說明非阻塞的優勢。

我的項目中要求使用SpringCloud-Gateway作爲微服務網關,這個網關框架使用的是WebFlux,完全使用了Reactor這個Reactive庫來進行編程,說實話我踩了無數的坑,由於國內Reactive的不普及,導致這方面的資料相當少,解決SpringCloud-Gateway的問題的時候,往往要去SpringCloud的Github裏的issue區尋找答案,在issue區裏,SpringCloud的一位Committer曾經說過幾句讓我疑惑的話:

下面提到被廢除的組件指的是在SpringCloud Hoxton版本中Reactive環境下被廢除

  • LoadBalancerClientFilter被廢除,因爲它是阻塞的API,被非阻塞的ReactiveLoadBalancerClientFilter替代
    在這裏插入圖片描述

  • HttpMessageConverters在Reactive中不被使用,因爲它是阻塞的API,被非阻塞的HttpMessageReader、HttpMessageWriter替代
    在這裏插入圖片描述

背景:HTTP請求包體由於都是raw的字節數組,我們需要根據Content-Type類型例如application/json,才知道要將字節數組轉化爲json字符串格式,這個工作就是由HttpMessageConverters來做的

這引發了我的思考,爲什麼他們是阻塞的就要被廢除?

仔細思考了一下,其實這倆組件被Reactive替代的關鍵點其實在於他們都存在一個潛在的阻塞IO動作,LoadBalancerClientFilter中使用了LoadBalancerClient使用阻塞式Http調用去註冊中心拿到服務地址,HttpMessageConverters使用阻塞式的流去讀寫Http的Body。

像Netty一樣,SpringCloudGateway(後簡稱Gateway)也使用了固定少量的線程去處理大量連接,祕訣是非阻塞的NIO,如果線程被阻塞在IO操作上,必然會影響吞吐量,因爲處理連接請求的線程一旦被阻塞,後面幾個連接請求必然得不到處理,從而降低了吞吐量,這也就是爲什麼Netty的使用過程中不能阻塞worker線程的原因。

這引發了我進一步思考,在Gateway中我也使用到了Feign組件完成微服務之間的調用,其底層我是使用的OKHttp去完成HTTP調用,其也是阻塞式的,如果我將其更換成Netty形式的非阻塞IO,是否能增加吞吐量呢?這隻能用測試來說明。我將Feign組件更換成Reactive形式,底層使用了Netty完成HTTP調用:

開3000個線程,每個線程分別循環10次和30次訪問,查看吞吐量的提升

測試一個比較複雜的處理接口,需要調用兩次Feign

  • 每個線程循環調用10次(阻塞式):500TPS

  • 每個線程循環調用10次(非阻塞式):300TPS

提升了66%的吞吐量


  • 每個線程循環調用30次(阻塞式):680TPS

  • 每個線程循環調用30次(非阻塞式):420TPS

提升了60%的吞吐量

不要吐槽TPS低,這裏測試的接口被測試機硬件性能和路由調用其他微服務的接口TPS限制,這裏僅僅關注吞吐量的提升幅度

原來一個潛在的阻塞IO操作會影響這麼多吞吐量(僅針對Gateway系統下來說,因爲在Gateway中,處理請求的線程數往往都很少,可能僅僅只是CPU核心*2的數量,這時候的阻塞IO操作真的會嚴重影響吞吐量,如果在傳統編程下,像阻塞的Servlet,一般服務器都直接開200多個線程處理請求,一個線程處理一個連接的請求,這時候阻塞一個線程沒什麼,有其他很多儲備的線程去處理其他連接的請求),這裏的吞吐量提升,使用NIO的Netty框架功不可沒,但這與Reactive有什麼關係呢?爲什麼我們非要在非阻塞上套上一個Reactive?Reactive在這裏解決了什麼問題?

1.2 異步非阻塞編程

看到這裏,應該能體會到非阻塞的"妙處"了,但是光有NIO是不夠的,我們需要一個異步非阻塞編程範式,爲什麼呢?因爲我們還有業務邏輯的處理啊,拿上面用戶登陸的例子來說,一個登陸步驟如下:

  1. 驗證A用戶請求是否合法
  2. 網絡調用查找A用戶賬號密碼
  3. 驗證賬號密碼是否一致

其中第一步和第三步因爲是CPU計算,是比較快的操作,由主線程來做,第二個操作是IO操作,由另外IO線程來做(如果是select多路複用模型,此時IO線程就負責註冊讀寫事件和調用select方法獲取已經到來的讀寫事件),傳統異步編程(傳統,略帶貶義的詞意可以看出這種編程模型存在一定缺陷)我們可以想到的方案是Callbacks回調和Future非阻塞提交的方案來做。

傳統的異步編程

Callbacks

耳熟能詳的Swing註冊監聽器,是典型的對於事件流進行響應的回調異步機制,個人感覺是Reactive鼻祖

使用回調方案

// 主線程線程池
ExecutorService mainThread = Executors.newFixedThreadPool(8);

// 驗證A用戶請求是否合法
doCheckRequest();
// 非阻塞調用,加入回調動作
httpRequest().addCallBacks(new Callback<Response>() {
        public void onSuccess(Response response) { 
					mainThread.submit(() ->{
            	// 主線程做
          	  checkUserInfo(response);
        	});
        }

        public void onError(Throwable error) { 
					// 異常處理
        }
      });
// 非阻塞,立即往下走了
// 接着處理B用戶的請求...

看起來確實能完成異步編程

Future

這裏我們使用CompletableFuture更好的完成操作

// 主線程線程池
ExecutorService mainThread = Executors.newFixedThreadPool(8);

// 驗證A用戶請求是否合法
doCheckRequest();
// 非阻塞調用,加入回調動作
httpRequest().thenComposeAsync(response -> {
					mainThread.submit(() ->{
            	// 主線程做
          	  checkUserInfo(response);
        	});
      });
// 非阻塞,立即往下走了
// 接着處理B用戶的請求...

似乎也能完成異步編程。

但這裏我們的場景十分簡單,如果是複雜的異步編程場景呢?這裏舉出Reactor文檔中的一個例子:

想象一個場景:我們現在需要展示用戶的前5名喜歡的東西在UI上,如果她沒有喜歡的東西,那麼就推薦給TA,這個場景需要3個服務(第一個是提供喜愛的物品ID列表,第二個是獲取喜愛東西的細節信息,第三個是推薦物品細節給用戶)

我們使用Callbacks來完成這個例子:

userService.getFavorites(userId, new Callback<List<String>>() { 
  // 獲取喜愛物品ID列表的回調動作
  public void onSuccess(List<String> list) { 
    // 如果是空,我們給TA推薦物品
    if (list.isEmpty()) { 
      suggestionService.getSuggestions(new Callback<List<Favorite>>() {
        // 需要獲取推薦細節,這裏依然回調動作
        public void onSuccess(List<Favorite> list) { 
          // 這裏拿到推薦細節,就可以放到UI上展示了
          UiUtils.submitOnUiThread(() -> { 
            list.stream()
                .limit(5)
                .forEach(uiList::show); 
            });
        }

        // 錯誤處理
        public void onError(Throwable error) { 
          UiUtils.errorPopup(error);
        }
      });
    } else {
      // 不爲空,那麼我們開始展示TOP5物品
      list.stream() 
          .limit(5)
          .forEach(favId -> favoriteService.getDetails(favId, 
            new Callback<Favorite>() {
              // 異步獲取物品細節的回調動作
              public void onSuccess(Favorite details) {
                UiUtils.submitOnUiThread(() -> uiList.show(details));
              }
              // 錯誤處理
              public void onError(Throwable error) {
                UiUtils.errorPopup(error);
              }
            }
          ));
    }
  }
  // 錯誤處理
  public void onError(Throwable error) {
    UiUtils.errorPopup(error);
  }
});

僅僅是這麼一點的業務邏輯,就需要寫這麼多回調接口,可讀性極差,編程難度極高,後期將難以維護,我們稱之爲回調地獄(Callback Hell)

Swing編程的EventListener也是類似的回調地獄

如果我們使用Reactor做同樣的事情呢?

// 獲取喜愛物品的ID列表
userService.getFavorites(userId) 
           .flatMap(favoriteService::getDetails)  // 將每一個物品都獲取其細節
           .switchIfEmpty(suggestionService.getSuggestions()) // 如果ID爲空,上面的flatMap不會觸發,這裏就會觸發爲空動作,獲取建議
           .take(5) // 獲取前5的信息
           .publishOn(UiUtils.uiThreadScheduler()) // 交給主UI線程去做
           .subscribe(uiList::show, UiUtils::errorPopup); // 開始觸發整個事件流,展示數據流最後的數據到UI上

如果我們需要增加異常處理?增加一行代碼即可

.onErrorResume(throwable -> {
  // handle error
})

如果我們需要限制getFavorites這個接口最多耗時800ms,超時要拋出異常,怎麼做?

userService.getFavorites(userId)
           .timeout(Duration.ofMillis(800)) // 800ms的超時處理
           .onErrorResume(cacheService.cachedFavoritesFor(userId)) // 異常處理
           .flatMap(favoriteService::getDetails) 
           .switchIfEmpty(suggestionService.getSuggestions())
           .take(5)
           .publishOn(UiUtils.uiThreadScheduler())
           .subscribe(uiList::show, UiUtils::errorPopup);

只需要增加兩行代碼,可讀性,操作可組合性都提升了,編程難度相對於Callbacks形式大幅下降

那麼Future在複雜編程中表現如何呢?

Future會逼callbacks好上那麼一點點,即使Java8推出了CompletableFuture,但仍然存在操作組合不太好做,組合多個Future對象編排在一起並不容易

  • 調用get()方法還是會在其他線程中阻塞
  • 不支持懶計算
  • 缺少多值數據流的處理,和一個比較好的錯誤處理機制

考慮另一個例子:我們獲取一個ID列表,然後根據ID獲取名稱和統計信息,將這兩個信息成對組合爲一個信息,使用CompletableFuture來完成這個例子:

// 獲取ID列表
CompletableFuture<List<String>> ids = ifhIds(); 

CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> { 
  // 獲取到ID列表後
	Stream<CompletableFuture<String>> zip =
			l.stream().map(i -> { 
    		// 遍歷ID列表
    		// 根據ID獲取名稱
				CompletableFuture<String> nameTask = ifhName(i); 
				// 根據ID獲取統計信息
				CompletableFuture<Integer> statTask = ifhStat(i); 

    		// 組合在一起
				return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); 
			});
  // 將結果整成List
	List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList()); 
	CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);

	CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray); 
	return allDone.thenApply(v -> combinationList.stream()
			// 阻塞等待future完成
			.map(CompletableFuture::join) 
			.collect(Collectors.toList()));
});

List<String> results = result.join(); 
assertThat(results).contains(
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121");

CompletableFuture組合操作雖然比callback好一些,但還是比較複雜,各自轉換來轉換去,然後纔可以組合,我們來看看同等組合操作使用Reactor的表現:

Flux<String> ids = ifhrIds(); 
// Flux表示發射多次值,每次發送一個ID
Flux<String> combinations =
  	// 每一個ID都做如下操作
		ids.flatMap(id -> { 
      // 獲取ID對應的name
			Mono<String> nameTask = ifhrName(id); 
      // 獲取ID對應的stat
			Mono<Integer> statTask = ifhrStat(id); 

      // 組合
			return nameTask.zipWith(statTask, 
					(name, stat) -> "Name " + name + " has stats " + stat);
		});

// 結果轉成一個List
Mono<List<String>> result = combinations.collectList(); 

List<String> results = result.block(); 
assertThat(results).containsExactly( 
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);

可以看出來,在Reactor中所有結果都是Mono或者Flux,想要組合在一起異常簡單,面向Mono或Flux組合即可。

到這裏可以看出,使用CallBacks或Future進行異步編程都分別有很多缺陷,這些缺陷就是reactive的目標想要去解決的。reactive使用消息發佈和對消息響應消費機制來完善這些缺陷。

我們需要reactive

引用自Reactor庫文檔

Reactive libraries, such as Reactor, aim to address these drawbacks of “classic” asynchronous approaches on the JVM while also focusing on a few additional aspects:

  • Composability and readability
  • Data as a flow manipulated with a rich vocabulary of operators
  • Nothing happens until you subscribe
  • Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high
  • High level but high value abstraction that is concurrency-agnostic

Reactive庫,例如Reactor,就是爲了去解決"傳統"異步編程的缺陷,主要致力於如下幾點構建:

  • 操作可組合性和可讀性
  • 數據可以像一個流一樣操作,並且有豐富的操作符使用
  • 在調用subscribe訂閱之前,沒有事情會發生(說明了一個懶計算機制,以及聲明式編程的特點)
  • 背壓,控制發佈數據端過快消費數據端過慢不平衡的問題
  • 屏蔽底層併發細節

其中最後一點可能暫時現在體會不大,但異步編程的一個特點就在於同樣一個流程ABCD做下來,有可能是a線程做的A流程,接着給b線程做的B流程,一個請求的處理不一定都是由同一個線程來做的,此時內部數據流傳遞的時候會有一些併發問題,Reactive庫屏蔽了這些併發問題(就像上面例子的ID列表的流式傳遞,就不需要考慮ID列表隨着流傳遞,發射到下游之後下游線程是否可見等等問題)。

開頭也說到,Reactive是一個設計模式,觀察者模式的擴展,類比與迭代器模式的拉取數據(next方法拉取數據)的方式,Reactive更像是一個等待被推數據的過程,觀察者觀察數據的到來,之後對數據作出響應,響應式名稱就由此而來,這是一個被動的過程,所以對於生產者生產消息的速率是不可控的,如果過快,消費者消費速率跟不上就會出現問題,因此Reactive引入了背壓技術,可以使用多種途徑對生產者生產消息做一個控制,從而達到平衡兩者速率的效果。

2. Reactive的定義

最後,我們結合一些文獻,嘗試給Reactive下一個定義

  • 引用維基百科:

Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s).

— https://en.wikipedia.org/wiki/Reactive_programming

這裏提到的關鍵在於,Reactive是一個異步編程範式,可以很好處理數據流的變化傳播(propagation of change)

  • 引用Spring5文檔:

The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.

There is also another important mechanism that we on the Spring team associate with “reactive” and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination.

Reactive Streams is a small spec (also adopted in Java 9) that defines the interaction between asynchronous components with back pressure. For example a data repository (acting as Publisher) can produce data that an HTTP server (acting as Subscriber) can then write to the response. The main purpose of Reactive Streams is to let the subscriber control how quickly or how slowly the publisher produces data.

Spring5認爲,reactive是一個圍繞變化響應的編程模型,變化響應指的是對一些IO事件(NIO)或者是例如鼠標事件等等。在這種層面上,非阻塞指的就是reactive,而不是被阻塞,reactive是可以在操作完成數據可用對通知作出反應的模式。

  • 引用Reactor庫文檔:

The reactive programming paradigm is often presented in object-oriented languages as an extension of the Observer design pattern. You can also compare the main reactive streams pattern with the familiar Iterator design pattern, as there is a duality to the Iterable-Iterator pair in all of these libraries. One major difference is that, while an Iterator is pull-based, reactive streams are push-based.

Reactor從實現角度去說明,reactive類比迭代器模式,前者是推數據模式,後者是主動拉數據模式,應對數據流作出響應則爲響應式編程

此時我們知道,Reactive是具有如下特點的:

  • 實現模式
    • Reactive Programming 作爲觀察者模式(Observer) 的延伸,不同於傳統的命令編程方式( Imperative programming)同步拉取數據的方式,如迭代器模式(Iterator) ,而是採用數據發佈者同步或異步地推送到數據流(Data Streams)的方案。
  • 響應式
    • 圍繞變化響應進行,變化可以是傳播數據流、傳播事件(IO事件、鼠標事件等),當該數據流(Data Steams)訂閱者監聽到傳播變化時,響應通知方面,包括操作完成(operations complete)和數據可用(data becomes available)。立即作出響應動作。
  • 可讀性好、可組合的、豐富的數據操作,進一步簡化異步編程的難度(屏蔽併發細節)
    • 在實現層面上,Reactive Programming 可結合函數式編程簡化面嚮對象語言語法的臃腫性,屏蔽併發實現的複雜細節,提供數據流的多種多樣的操作,從而達到提升代碼的可讀性,以及減少 Bugs 出現的目的。
  • 背壓支持
    • Reactive Programming 結合背壓(Backpressure)的技術解決發佈端生成數據的速率高於訂閱端消費的問題。

希望達到使用少量的系統資源,提升應用的伸縮性的目的,我們需要使用異步編程。而響應式的推模式編程確實能夠解決傳統異步編程的各項缺點。使用Reactive這種聲明式的異步編程範式提升應用伸縮性,在我看來是之後的一種大趨勢。

3. Reactive適用性

Reactive異步非阻塞編程雖好,但目前也存在一些挑戰,並不是人人都能做的,這裏引用Spring5官方文檔的關於SpringMVC和SpringWebFlux切換建議:

A simple way to evaluate an application is to check its dependencies. If you have blocking persistence APIs (JPA, JDBC) or networking APIs to use, Spring MVC is the best choice for common architectures at least. It is technically feasible with both Reactor and RxJava to perform blocking calls on a separate thread but you would not be making the most of a non-blocking web stack.

Spring5這裏提到的觀點是,如果你有阻塞的API例如JDBC或者一些網絡調用,SpringMVC則是你的最佳選擇,就像文章中間我舉的一個現實生產中的一個例子一樣,阻塞在WebFlux中相當影響吞吐量,所以不管是數據庫調用、Redis調用、微服務之間潛在的網絡調用,都需要使用Reactive,目前不知道JDBC是否有Reactive版本,Redis已經有Spring封裝lettuce的API做的Reactive版本,Feign則需要尋找第三方開源人士提供的Reactive版本,所以我想說的是,如果你找不到對應的Reactive版本的API,那就不要使用WebFlux構建你的系統,吞吐量會降低,因爲這樣會阻塞主線程。很多人不知道這一點的關鍵性,盲目使用Gateway然後發現自己的吞吐量很低,開始吐槽Gateway或是reactive沒有傳聞中那麼神,此時更需要想想系統中是否存在潛在的阻塞API。

If you have a large team, keep in mind the steep learning curve in the shift to non-blocking, functional, and declarative programming. A practical way to start without a full switch is to use the reactive WebClient. Beyond that, start small and measure the benefits. We expect that, for a wide range of applications, the shift is unnecessary. If you are unsure what benefits to look for, start by learning about how non-blocking I/O works (for example, concurrency on single-threaded Node.js) and its effects.

這裏提到的關鍵點在於學習路線相當陡峭(steep learning curve in the shift to non-blocking),最基本的你需要知道非阻塞IO的作用,知道Spring的組件WebClient的使用,還得學習Reactive豐富的操作符,這樣纔可以用的好WebFlux,可謂是高端玩家的遊戲,其對於團隊的整體能力要求還是很高的。此時你需要評估學習成本和帶來的收益,如果不是非常高併發的系統,我這裏還是建議傳統的SpringMVC。

If you have a Spring MVC application that works fine, there is no need to change. Imperative programming is the easiest way to write, understand, and debug code. You have maximum choice of libraries, since, historically, most are blocking.

最後,還是那句話,如果你的SpringMVC應用可以良好工作,就沒必要更換WebFlux。使用過Reactive的小夥伴一定知道調試和查看Reactive的異常堆棧是一件很困難的事情。傳統命令式編程還是更容易寫、理解和debug的,所使用的阻塞API的類庫選擇很多並且都已經成熟。

想入Reactive這個大坑,準備好深入學習理解再說吧,不然很容易得不償失。

4. 參考文獻

  • 《Reactive Programming 一種技術,各自表述》 https://www.imooc.com/article/46306
  • 《Reactor庫官方文檔》https://projectreactor.io/docs/core/release/reference/#intro-reactive
  • 《Spring5官方文檔》https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-why-reactive
  • 《維基百科Reactive_Programming》https://en.wikipedia.org/wiki/Reactive_programming
  • 《reactive-streams-jvm》https://github.com/reactive-streams/reactive-streams-jvm
  • 《The Reactive Manifesto》https://www.reactivemanifesto.org/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章