響應式流的核心機制——背壓機制

涅槃~小白發表於2024-04-06

一、響應式流是什麼?

Reactive Streams 是 2013 年底由 Netflix、Lightbend 和 Pivotal(Spring 背後的公司)的工程師發起的一項計劃,響應式流旨在為無阻塞非同步流處理提供一個標準。它旨在解決處理元素流的問題——如何將元素流從釋出者傳遞到訂閱者,而不需要釋出者阻塞,或訂閱者有無限制的緩衝區或丟棄。

響應式流模型存在兩種基本的實現機制。一種就是傳統開發模式下的“拉”模式,即消費者主動從生產者拉取元素;而另一種就是“推”模式,在這種模式下,生產者將元素推送給消費者。相較於“拉”模式,“推”模式下的資料處理的資源利用率更好,下圖所示的就是一種典型的推模式處理流程。

在這裡插入圖片描述
上圖中,資料流的生產者會持續地生成資料並推送給消費者。這裡就引出了流量控制問題,即如果資料的生產者和消費者處理資料的速度是不一致的,我們應該如何確保系統的穩定性呢?

二、流量控制

2.1 生產者生產資料的速率小於消費者的場景

這種場景對於消費者來說沒啥壓力,正常消費就好了,這裡也就不需要所謂的流量控制了。

2.2 生產者生產資料的速率大於消費者的場景

生產者生產資料的速率大於消費者的場景,應該是我們業務中經常遇到的場景了,這種場景由於消費者處理不過來導致崩潰,業界通常的做法是在生產者與消費者之間加一個佇列做緩衝。我們知道佇列具有儲存與轉發的功能,所以可以用它來進行一定的流量控制。

在這裡插入圖片描述
如何對於流量進行很好的控制?這就轉變到了如何設計好一個佇列了,目前 Java 業界主流的佇列有以下三種:

2.2.1 無界佇列

見名知意,無界佇列在原則上是擁有無線大小容量的佇列,可以存放生產者產生的所有訊息。

在這裡插入圖片描述

  • 優勢:確保消費者消費到所有的資料
  • 劣勢:系統的回彈性降低,任何一個系統不可能擁有無限的資源,一旦記憶體等資源耗盡,系統就可能會有崩潰的風險。

2.2.2 有界丟棄佇列

為了避免上面無界佇列的弊端,有界丟棄佇列採用的是如果佇列滿了,就會採用丟棄後面傳入的值,這裡可以設定一些丟棄策略,比如說按照優先順序或先進先出等。

在這裡插入圖片描述

  • 優勢:考慮到資源的限制,適合允許丟訊息的業務場景。
  • 劣勢:訊息重要性很高的場景不建議採取這種佇列

2.2.3 有界阻塞佇列

像一些支付金融級別的場景,是不允許丟資料的,所以我們引出有界阻塞佇列,我們會在佇列訊息數量達到上限後阻塞生產者,而不是直接丟棄訊息。

在這裡插入圖片描述

  • 優勢:解決了不允許丟資料的業務場景
  • 劣勢:當佇列滿了的時候,會阻塞生產者停止生產資料,這種場景不可能實現非同步操作的。

所以,無論從回彈性、彈性還是即時響應性出發,上述的佇列都不是響應式流的上佳解決辦法。

三、背壓機制

上面說的那幾種佇列純“推”模式下的資料流量會有很多不可控制的因素,並不能直接應用,而是需要在“推”模式和“拉”模式之間考慮一定的平衡性,從而優雅地實現流量控制。這就需要引出響應式系統中非常重要的一個概念——背壓機制(Backpressure)。

什麼是背壓?簡單來說就是下游能夠向上遊反饋流量請求的機制。透過前面的分析,我們知道如果消費者消費資料的速度趕不上生產者生產資料的速度時,它就會持續消耗系統的資源,直到這些資源被消耗殆盡。

這個時候,就需要有一種機制使得消費者可以根據自身當前的處理能力通知生產者來調整生產資料的速度,這種機制就是背壓。採用背壓機制,消費者會根據自身的處理能力來請求資料,而生產者也會根據消費者的能力來生產資料,從而在兩者之間達成一種動態的平衡,確保系統的即時響應性。

四、響應式流規範

有了背壓機制,我們再來看下響應式流是如何基於這種機制去設計的一套規範,規範詳情請參考:Reactive Streams

Java API 的響應式流只定義了四個核心介面:

  • Publisher<T>
  • Subscriber<T>
  • Subscription
  • Processor<T,R>

4.1 Publisher<T>

Publisher 代表的就是一種可以生產無限資料的釋出者,介面如下:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

可以看到,Publisher 裡的 subscribe 方法傳入的是 Subscriber 介面,其實這裡用的是回撥,Publisher 根據收到的請求向當前訂閱者 Subscriber 傳送元素。

4.2 Subscriber<T>

Subscriber 代表的是一種可以從釋出者那裡訂閱並接收元素的訂閱者,介面如下:

public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}

Subscriber 介面定義的這組方法構成了資料流請求和處理的基本流程,其中,onSubscribe() 從命名上看就是一個回撥方法,當釋出者的 subscribe() 方法被呼叫時就會觸發這個回撥。而在該方法中有一個引數 Subscription,可以把這個 Subscription 看作是一種用於訂閱的上下文物件。Subscription 物件中包含了這次回撥中訂閱者想要向釋出者請求的資料個數。

當訂閱關係已經建立,那麼釋出者就可以呼叫訂閱者的 onNext() 方法向訂閱者傳送一個資料。這個過程是持續不斷的,直到所傳送的資料已經達到 Subscription 物件中所請求的資料個數。這時候 onComplete() 方法就會被觸發,代表這個資料流已經全部傳送結束。而一旦在這個過程中出現了異常,那麼就會觸發 onError() 方法,我們可以透過這個方法捕獲到具體的異常資訊進行處理,而資料流也就自動終止了。

4.3 Subscription

Subscription 代表的就是一種訂閱上下文物件,它在訂閱者和釋出者之間進行傳輸,從而在兩者之間形成一種契約關係,介面如下:

public interface Subscription {
    public void request(long n);
    public void cancel();
}

這裡的 request() 方法用於請求 n 個元素,訂閱者可以透過不斷呼叫該方法來向釋出者請求資料;而 cancel() 方法顯然是用來取消這次訂閱。請注意,Subscription 物件是確保生產者和消費者針對資料處理速度達成一種動態平衡的基礎,也是流量控制中實現背壓機制的關鍵所在

在這裡插入圖片描述

4.4 Processor<T,R>

Processor 代表的就是訂閱者和釋出者的處理階段,Processor 介面繼承了 Publisher 和 Subscriber 介面。 它用於轉換髮布者——訂閱者管道中的元素。 Processor<T,R> 訂閱型別 T 的資料元素,接收並轉換為型別 R 的資料,併發布變換後的資料。介面如下:

public interface Processor<T,R> extends Subscriber<T>, Publisher<R> {
}

下圖顯示了處理者在釋出者——訂閱和管道中作為轉換器的作用,可以擁有多個處理者。

在這裡插入圖片描述

五、總結

  • 響應式流規範定義的很簡潔,但實現起來並不簡單,釋出者和訂閱者之間的所有互動的非同步性質以及背壓機制使得實現變得複雜。
  • 響應式流規範非常靈活,還可以提供獨立的“推”模型和“拉”模型。如果為了實現純“推”模型,我們可以考慮一次請求足夠多的元素;而對於純“拉”模型,相當於就是在每次呼叫 Subscriber 的 onNext() 方法時只請求一個新元素。
  • JDK 9 中提供了 Flow 響應式流介面,與響應式流相容的介面,可以看得出,JDK 團隊後續的發展趨勢也是想往響應式流這塊靠近。

相關文章