Reactive Streams の導入とアプリケーション分析

目次

1. Reactive Streams の基礎知識

(1) 基本的な紹介

(2) 反応流の特徴

基本機能1:イベントドリブン&チェンジデリバリー

基本機能2:データの流れ

基本機能 3: 宣言型

先進機能1:流量制御(背圧)

高度な機能 2: 非同期境界

(3) リアクティブフローインターフェース

2. 業務アプリケーションサンプルコード表示

(1) 具体的な枠組みの紹介

(2) 業務アプリケーションコードの表示例

例 1: ファイルの内容を読み取り、コンソールに出力するために使用されます。

例 2: Twitter のリアルタイム データ ストリームからツイートを読み取り、コンソールに出力します。

例3:製品情報を取得し、指定した条件でソートしてコンソールに出力する

3. リアクトル原理の解析

通話関係

実装プロセス

背圧

非同期境界

4. 業務用途でのご提案

参考文献、書籍、リンク


1. Reactive Streams の基礎知識

(1) 基本的な紹介

Reactive Streams は非同期ストリーム処理に基づいた標準仕様であり、ストリーム処理の信頼性、効率性、応答性を高めることを目的としています。

 Reactive Streams の中心となるアイデアは、パブリッシャー (Publisher) とサブスクライバー (Subscriber) の間で非同期ストリーム処理を可能にして、ノンブロッキングのリアクティブ アプリケーションを実現することですパブリッシャーは任意の数の要素を作成してサブスクライバーに送信し、サブスクライバーは要素を非同期に処理できます。Reactive Streams は、ストリーム処理の正確さと信頼性を保証するために、次のようないくつかのインターフェイスとプロトコルも定義します。

  • パブリッシャー: 要素を生成し、サブスクライバーに送信するメソッドを定義します。
  • サブスクライバー: 要素を受信して​​処理するメソッドを定義します。
  • サブスクリプション: リクエスト要素やサブスクリプション解除など、サブスクライバーとパブリッシャーの間の合意を定義します。
  • プロセッサ: パブリッシャ インターフェイスとサブスクライバ インターフェイスの両方を実装するミドルウェアを定義し、要素を変換またはフィルタリングできます。

Reactive Streams には幅広いアプリケーション シナリオがあり、ネットワーク通信、データベース アクセス、画像処理など、大量のデータ ストリームの処理が必要なあらゆるシナリオに適用できます。また、Spring Reactor、Akka Streams など、多くのストリーム処理フレームワークやライブラリの基礎でもあります。

(2) 反応流の特徴

リアクティブ フローの特性は簡単に言うと、基本特性 (変更配信 + データ フロー + 宣言) + 高度な機能 (ノンブロッキング バックプレッシャー + 非同期境界) です。

基本機能1:イベントドリブン&チェンジデリバリー

リアクティブ ストリーミングの核心はイベント駆動型の変更配信であり、パブリッシャーがデータを生成すると、データは次のコンポーネントにプッシュされ、コンポーネント間を継続的に渡されて、最終的に最終コンシューマに到達します。

イベント駆動型および変更配信とは何ですか? まずは古典的なケースを見てみましょう

図のように、セルC1に数式「=SUM(A1:B1)」を入力すると、セルA1またはB1のデータがどのように変化しても、それらの合計C1はすぐに変化します。具体的には、データが変化したとき、C1を制御してデータや計算を行う必要がありません(「C1 = SUM(A1:B1)」などのプログラムを呼び出す必要がありません)。このアクションはイベント駆動型であることを示します。イベント駆動型ではコンポーネントを時間次元で分離できるため、開発者は並行プログラミングを実行しやすくなります。

では、釣銭配信とは何でしょうか? 以下の行 3 に示すように Excel の例を展開し、D1 を SUM(B1:C1) に設定し、E1 を SUM(C1:D1) に設定する...というように、各値は前の 2 つの値に依存します。フィボナッチ数列が生成され、A1 または B1 が変化すると、ドミノ倒しのように、それを直接および間接的に参照するデータが変化します。これが変化伝達です

基本機能2:データの流れ

変更の配信などの情報はデータ フローに基づいています。

図に示すように、データ ストリームは、時間内に順序付けされた進行中のイベントのシーケンスです。このシーケンスには、開始イベント、データ処理イベント、エラー イベント、および終了イベントが含まれます

クリック イベントが発生した場合、データ ストリーム上のイベントを監視し、これらのイベントの発生に基づいて特定の関数をトリガーするだけで済みます。このプロセスはオブザーバー パターンに似ています。データ ストリーム上では、各イベントがプロデューサーとしてデータを生成し、これらのイベントに対応する 1 つ以上のオブザーバーが存在します。

Excel の例に戻ると、開始イベント、データ処理イベント、および単純な終了イベント (G3) を含む単純なフィボナッチ数列が行 3 で定義されています。A1 または B1 が変化すると、オブザーバー C1 の対応する A 変化により、C1 のオブザーバーが発生します。 D1 を変更するなど。A1 と B1 にはオブザーバーが 1 つだけありますが、B1、C1... には複数のオブザーバーがあります。

リアクティブ ストリーミングは確かにオブザーバー パターンの拡張です。リアクティブ ストリーミングでは、このパブリッシュとサブスクライブの関係をより簡単に定義できます。さらに、リアクティブ ストリーミングのサブスクリプション目標はより詳細です。通常、オブザーバー パターンのパブリッシャーとサブスクライバーは特定です。クラス、一方、リアクティブストリームは具体的なプロパティにすることができます

基本機能 3: 宣言型

リアクティブ フローでは、変更の配信はデータ フローに基づいて行われますが、データ フローはどのように定義されるのでしょうか? リアクティブ ストリーミングでは、プロデューサーは情報を生成することのみを担当し、開発者が行う必要があるのは、コンシューマーが変更を配信するための計算ロジックを事前に定義することだけです次のコードに示すように、宣言型プログラミングを使用してデータ処理パイプラインを定義できます。

data.stream()
  .map(o -> o * o)
  .map(Math::sqrt)
  .map(...)
  ...

これらのパイプラインが定義されると、受信データが何であっても、追加の制御プロセスを定義する必要なく、パイプラインを通じて処理されます。

次のコードに示すように、命令型プログラミングでは、a の 2 番目の代入は b の値に影響しません。b の値を更新する場合は、b = a + 1 を繰り返し実行する必要があります。ただし、リアクティブフローでは、b = a + 1 を宣言した後、b はある計算の結果を保存するのではなく、計算ロジックを保存し、a の値の変化に応じていつでも b を変更できます。

a = 1;
b = a + 1;  //在命令式编程中,b保存的是计算的结果。在反应式编程中,b保存的是计算逻辑
a = 2;      //在反应式编程中,a的变化会直接引起b的变化。在命令式编程中,需要重复执行代码,才回改变b的值。

このため、リアクティブ フローはプロセス管理の抽象化に役立ち、ビジネス間の論理的な関係を構築および保存できます。

先進機能1:流量制御(背圧)

リアクティブ ストリーミングでは、データ ストリームのプロデューサーはパブリッシャーと呼ばれ、コンシューマーはサブスクライバーと呼ばれます。パブリッシャとサブスクライバのデータ処理速度が一致しない、次のような問題が発生します。

  • パブリッシャーの生産速度がサブスクライバーの処理速度よりも高い: サブスクライバーはデータに圧倒され、圧倒されます。
  • サブスクライバの消費レートがパブリッシャの処理レートよりも大きい: リソースの使用率が不十分です

では、生産率と消費率のバランス「だけ」を維持し、この危機的な状態を継続する方法はあるのでしょうか? この問題を解決するには、データの流れの速度を制御する必要があり、Subscriberが自分の消費電力をPublisherにフィードバックできるようにする必要があります。この仕組みは「バックプレッシャー」と呼ばれる通信速度の調整方法です。コンポーネント間の手段

図に示すように、リアクティブ フローでは、実際にデータ処理の中間段階を表すのはオペレーターです。オペレーターはプロデューサーとコンシューマーの両方です。これらを接続するとパイプラインが形成されます。パイプラインには、サブスクライバーとのパイプラインがあります。開始点からの上向きの呼び出しチェーンは、リアクティブ フローのバックプレッシャー メソッドであるため、このバックプレッシャー リンクを通じて、コンシューマーは自身の消費機能を上流のプロデューサーに渡すことができます。

高度な機能 2: 非同期境界

リアクティブ ストリーミングでは、データがコンポーネント間で非同期に転送され、パブリッシャーをブロックできないことが規定されています。なぜこれが規定されているのでしょうか?非同期によりスループットが向上するため、Node、OpenResty などの一部の高性能技術手段は非同期モデルに基づいています。

リアクティブ ストリーミングも非同期に基づくブロッキング問題を解決しますが、リアクティブ ストリーミングには次の制約が課され、これにより「古典的な」JVM 非同期メソッド (コールバックとフューチャー) によって引き起こされる欠点が解決され、プログラムのプログラマビリティと可読性が向上します。

まず、リアクティブ ストリームでは、バックプレッシャーが非ブロッキングでなければならないことが強制されます。バックプレッシャーが同期の場合、非同期処理は無効になります。

次に、リアクティブ フローでは、コンポーネント間のデータ転送が非同期であり、パブリッシャーをブロックできないことが規定されています。

図に示すように、nioSelectorThreadOrigin と toNioSelectorOutput は非同期プロデューサーとコンシューマーを表し、パイプ記号「|」はコンポーネント間の非同期境界を表し (境界に使用される技術的手段は関係ありません)、R# はスレッド リソース、R2、R3、 R4 これらはすべて非同期で実行され、特定のイベント モデルまたはマルチスレッド スケジューリングに基づいてスケジュールされる場合があります。

最後に、リアクティブ ストリームは、リソース管理とスケジューリングにおいてより柔軟です。コンポーネントにはいくつかの同期処理ロジックを含めることができ、コンポーネント間のデータ転送が非同期であることを確認することのみが必要です。

図に示すように、最初の例では、R1 コンポーネントは元のスレッド上でマップとフィルターを同期的に処理できますが、2 番目の例では、R2 がマップとフィルターを同期的に処理する必要があります。

具体的なケースの利点の例: Reactor 公式 Web サイトでは、シンプルな getFavorites ビジネス シナリオが提供されています。具体的なビジネス ロジックは次のとおりです。ユーザーの好きなもののリストを取得します。空の場合は 3 つが提案されます。空でない場合は、詳細を取得し、最後にUIに表示します。

「古典的な」JVM は Callback+CompletableFuture という非同期問題をどのように解決しますが、より複雑なビジネス プロセス オーケストレーションになると、その問題が明らかになります: Callback は問題を解決できますが、簡単にコールバック地獄に陥る可能性があります。CompletableFuture もあります。コードの可読性の問題を回避する方法はありません。以下は、Callback メソッドを使用して、上記の getFavorites ビジネス シナリオを解決するコード例です。2 層のコールバック ロジックだけでは、少し疲れます。具体的なコードは次のとおりです。

userService.getFavorites(userId, new Callback<List<String>>() { 
  public void onSuccess(List<String> list) { 
    if (list.isEmpty()) { 
      suggestionService.getSuggestions(new Callback<List<Favorite>>() {
        public void onSuccess(List<Favorite> list) { 
          UiUtils.submitOnUiThread(() -> { 
            list.stream()
                .limit(5)
                .forEach(uiList::show); 
            });
        }
​
        public void onError(Throwable error) { 
          UiUtils.errorPopup(error);
        }
      });
    } else {
      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);
  }
});

リアクティブ ストリームを使用すると、非同期性と読みやすさの問題を簡単に解決できます。コードは次のとおりです。

userService.getFavorites(userId)                               // R1
           .flatMap(favoriteService::getDetails)               // R1
           .switchIfEmpty(suggestionService.getSuggestions())  // R1
           .take(5)                                            // R1
           .publishOn(UiUtils.uiThreadScheduler())             // 异步边界
           .subscribe(uiList::show, UiUtils::errorPopup);      // R2

(3) リアクティブフローインターフェース

リアクティブ ストリーミング仕様では、4 つのインターフェイスと 7 つのメソッドが定義されており、各インターフェイスの実装にいくつかの制約が課されます。

// 发布者接口
public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}
// 订阅者接口
public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}
// 订阅关系接口
public interface Subscription {
    public void request(long n);
    public void cancel();
}
// 执行者接口:用于转换发布者到订阅者之间管道中的元素。它既是一个订阅者又是一个发布者。
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

以下のインターフェイス定義から、非同期を実現するために、すべてのインターフェイス メソッドには戻り値がないことがわかります。特定のソース コード分析では、プレゼンテーション分析は実行されません。

2. 業務アプリケーションサンプルコード表示

(1) 具体的な枠組みの紹介

いくつかのストリーム処理フレームワークと関連ライブラリを直接使用できます。一般的に使用される 2 つのフレームワークの参照例を次に示します。

  • 最初の Akka Streams アプリケーションでは、次の Maven 依存関係を含める必要があります。
<dependencies>
    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-stream_2.12</artifactId>
        <version>2.6.16</version>
    </dependency>
    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-stream-testkit_2.12</artifactId>
        <version>2.6.16</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-stream-kafka_2.12</artifactId>
        <version>2.1.1</version>
    </dependency>
</dependencies>
  • 2 番目の Reactor アプリケーションには、次の Maven 依存関係を含める必要があります。
<dependencies>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-core</artifactId>
        <version>3.4.8</version>
    </dependency>
</dependencies>

(2) 業務アプリケーションコードの表示例

例 1: ファイルの内容を読み取り、コンソールに出力するために使用されます。

package org.zyf.javabasic.reactivestreams;

import reactor.core.publisher.Flux;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * @author yanfengzhang
 * @description 使用 Reactor 框架实现的反应式流应用举例代码,用于读取文件内容并将其输出到控制台
 * @date 2023/5/1  19:43
 */
public class FileContentPrint {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("test.txt");

        // 创建 Flux 对象并读取文件内容
        Flux<String> fileContent = Flux.using(
                () -> Files.lines(path),
                Flux::fromStream,
                stream -> {
                    try {
                        stream.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
        );

        // 订阅 Flux 对象并输出文件内容
        fileContent.subscribe(System.out::println);
    }
}

上記のコードでは、 Flux.using() メソッドを使用して Flux オブジェクトを作成し、指定されたパスにあるファイルの内容を読み取ることによってデータ ストリームを生成します。次に、subscribe() メソッドを使用して Flux オブジェクトをサブスクライブし、ラムダ式を介して各要素をコンソールに出力します。Reactor フレームワークを使用しているため、このアプリケーションは完全にリアクティブなストリーミング アプリケーションであり、ファイルの読み取りと要素の出力のプロセスは非同期かつノンブロッキングであり、コンピューター リソースを有効に活用できます。

例 2: Twitter のリアルタイム データ ストリームからツイートを読み取り、コンソールに出力します。

package org.zyf.javabasic.reactivestreams;

import akka.Done;
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.stream.ActorMaterializer;
import akka.stream.Materializer;
import akka.stream.javadsl.Source;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.TwitterSource;
import com.typesafe.config.ConfigFactory;

import java.util.concurrent.CompletionStage;

/**
 * @author yanfengzhang
 * @description 使用 Akka Streams 实现的反应式流应用举例代码,用于从 Twitter 实时数据流中读取推文并将其输出到控制台
 * @date 2023/5/1  19:47
 */
public class TweetsPrint {
    public static void main(String[] args) throws Exception {
        // 创建 Actor 系统和 Materializer
        ActorSystem system = ActorSystem.create("reactive-streams-example", ConfigFactory.load());
        Materializer materializer = ActorMaterializer.create(system);

        // 创建 TwitterSource 对象并订阅推文数据流
        Source<String, NotUsed> tweets = TwitterSource.create(
                "Consumer Key",
                "Consumer Secret",
                "Access Token",
                "Access Token Secret"
        ).map(status -> status.getText());

        // 创建 Sink 对象并将推文输出到控制台
        Sink<String, CompletionStage<Done>> consoleSink = Sink.foreach(System.out::println);

        // 将 Source 和 Sink 连接起来,并运行流处理程序
        tweets.runWith(consoleSink, materializer);
    }

}

上記のコードでは、TwitterSource.create() メソッドを使用して Source オブジェクトを作成します。このオブジェクトは、Twitter リアルタイム データ ストリームからツイート データを読み取り、データ ストリームを生成します。次に、各ツイートをコンソールに出力するための Sink オブジェクトが作成されます。最後に、ソースとシンクを接続し、runWith() メソッドを使用してストリーム ハンドラーを実行します。Akka Streams フレームワークを使用しているため、このアプリケーションは完全にリアクティブなストリーミング アプリケーションであり、ツイートの読み取りと要素の出力プロセスは非同期かつノンブロッキングであり、コンピューター リソースを有効に活用できます。

例3:製品情報を取得し、指定した条件でソートしてコンソールに出力する

package org.zyf.javabasic.reactivestreams;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @author yanfengzhang
 * @description 使用 Reactor 框架实现的电商场景应用举例代码,用于获取商品信息并将其按照指定条件进行排序并输出到控制台
 * @date 2023/5/1  19:50
 */
public class ProductPrint {
    public static void main(String[] args) throws Exception {
        // 创建商品对象列表
        List<Product> productList = Arrays.asList(
                new Product("A001", "商品A", new BigDecimal("100.00")),
                new Product("A002", "商品B", new BigDecimal("200.00")),
                new Product("A003", "商品C", new BigDecimal("300.00")),
                new Product("A004", "商品D", new BigDecimal("400.00")),
                new Product("A005", "商品E", new BigDecimal("500.00"))
        );

        // 创建 Flux 对象并按照价格排序
        Flux<Product> productFlux = Flux.fromIterable(productList)
                .sort(Comparator.comparing(Product::getPrice));

        // 订阅 Flux 对象并输出商品信息
        productFlux.subscribe(product -> System.out.println(product.getId() + " - " + product.getName() + " - " + product.getPrice()));
    }

    // 商品对象类
    static class Product {
        private String id;
        private String name;
        private BigDecimal price;

        public Product(String id, String name, BigDecimal price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public BigDecimal getPrice() {
            return price;
        }

        public void setPrice(BigDecimal price) {
            this.price = price;
        }
    }
}

上記のコードでは、商品オブジェクトのリストが作成され、Flux.fromIterable() メソッドを使用して Flux オブジェクトに変換されます。次に、sort() メソッドを使用して製品の価格で並べ替え、ラムダ式を使用して各製品の ID、名前、価格をコンソールに出力します。Reactor フレームワークを使用しているため、このアプリケーションは完全にリアクティブなストリーミング アプリケーションであり、製品情報の取得と要素の出力のプロセスは非同期かつノンブロッキングであり、コンピューター リソースを有効に活用できます。

3. リアクトル原理の解析

Reactor の Flux を例として、その実行原理を説明します。

Reactor クラス ライブラリには 2 種類のデータ パブリッシャーがあり、1 つは 0 または N 要素の非同期キューを表す Flux で、もう 1 つは 0 または 1 要素の非同期キューを表す Mono です。

// log 函数可以打印Flux的调用过程
Flux.just(1, 2, 3)
  .log()
  .map(o -> {
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return o * o;
  })
  .log()
  .filter(o -> o > 0)
  .log()
  .subscribe();

通話関係

Flux.just(1, 2, 3).subscribe() を例として、パブリッシャー、サブスクライバー、およびサブスクリプション間の呼び出し関係を説明します。

上記は Operator が存在しない場合の Publisher、Subscriber、Subscription の呼び出し関係です。サブスクライブ関数が実行された後、次のことを実行します。

  • サブスクリプションを開始します。
  • PublisherPublisher は新しい ArraySubscription SN を作成しました。
  • サブスクライバのコールバック onSubscribe 関数を通じて sn をサブスクライバ sub に渡します。
  • サブスクライバ sub は、sn を通じて request(n) リクエストを開始します。
  • パブリッシャーは sn を介して sub.onNext() 呼び出しを開始します。
  • パブリッシャーのシーケンスが終了するかエラーが発生した場合、情報はサブスクライバーの onComplete/onError を通じて渡されます。

実装プロセス

上記のコードの実行から、実行プロセスは 3 つのステージに分割できることがわかります。onSubscribe ステージ、リクエスト ステージ、onNext ステージに、計算ロジック パイプラインのサブスクリプション プロセスと宣言プロセスを加えて、5 つのステージがあります。合計 - 宣言ステージ、サブスクライブフェーズ、onSubscribe フェーズ、リクエストフェーズ、および onNext フェーズ。

// 执行结果, 此时整个数据链路不存在异步边界,完全同步执行
// onSubscribe
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxArray.ArrayConditionalSubscription)
[ INFO] (main) | onSubscribe([Fuseable] FluxMapFuseable.MapFuseableConditionalSubscriber)
[ INFO] (main) | onSubscribe([Fuseable] FluxFilterFuseable.FilterFuseableSubscriber)
// request
[ INFO] (main) | request(unbounded)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | request(unbounded)
// onNext 消费数据
[ INFO] (main) | onNext(1)
[ INFO] (main) | onNext(1)
[ INFO] (main) | onNext(1)
[ INFO] (main) | onNext(2)
[ INFO] (main) | onNext(4)
[ INFO] (main) | onNext(4)
[ INFO] (main) | onNext(3)
[ INFO] (main) | onNext(9)
[ INFO] (main) | onNext(9)
// onComplete 没数据了
[ INFO] (main) | onComplete()
[ INFO] (main) | onComplete()
[ INFO] (main) | onComplete()

宣言段階

サブスクライバーが実際にサブスクリプションを開始する前に、最初にデータ処理パイプラインを宣言する必要があります。パイプラインでは、各 Operator は実際には上流のパブリッシャーのカプセル化であるため、全体として、パイプラインはパブリッシャーによって層ごとにラップされたタマネギに似ています。この章の冒頭にある Flux.just(1, 2, 3).map().filter() を例として取り上げます。次の図は、パブリッシャーの段階的なカプセル化プロセスを示しています。最初のパブリッシャーにはデータのみが含まれています、後続の各演算子は、前のパブリッシャー オブジェクトのカプセル化にもデータ処理ロジックが含まれており、最終的に最終的なパブリッシャー fluxFilter を形成します。

購読ステージ

実行パイプラインのパイプラインはタマネギに似ており、サブスクリプション プロセスもタマネギに似ています。唯一の違いは、パイプラインが上流から下流へのパッケージ化プロセスであるのに対し、サブスクリプション プロセスは下流から上流へのパッケージ化プロセスであることです。上図に示すように、サブスクライブ操作中、リアクティブ フローは最も外側のソース サブスクライバーを層ごとにラップし、ラップされたオペレーターに基づいてさまざまなサブスクライバーを作成します。

購読ステージ上

onSubscribe 呼び出しにより、すべてのコンポーネントがチェーンのように関連付けられます。転送プロセスは上流から下流へ行われます。具体的なプロセスは、下流の (Enhanced)Subscriber.onSubscribe メソッドを呼び出し、それ自体を入力パラメータとして使用することです。

リクエストステージ

サブスクライバはデータ ソースに戻るまで上向きにデータを要求します。入力パラメータ n は要求されたデータの量を表します。

呼び出しフェーズ

データ要素は水道管の中の水のようなもので、最後のサブスクライバー Subscriber まで (拡張) Subscriber.Subscriber の onNext() を通過します。

パイプラインをサブスクライブした後のフローチャートは次のとおりです。

背圧

ノンブロッキング背圧は、リアクティブフローの最も重要な特徴です。Reactor パイプライン内にはボトムアップのサブスクリプション チェーンがあり、 request(n) リクエストを渡すことができます。リクエスト関数のパラメータ n は、バックプレッシャー実装の基礎となるアップストリーム データに対するダウンストリーム リクエストの数を表します。

 実際、Reactor では、「バックプレッシャー」はバックプレッシャー コンポーネントを通じて実装され、各コンポーネントには異なる戦略があります。Reactor は、次の 5 種類のバックプレッシャー戦略をサポートしています。

  • エラー: IllegalStateException をスローします。
  • ドロップ:捨てる。
  • 最新: 最新の値を返します。
  • バッファ: キャッシュ、キャッシュ サイズを設定できます。キャッシュが多すぎると OOM 例外が発生します。
  • 無視: ダウンストリーム要求リクエストを完全に無視します。これにより、ダウンストリーム キューがバーストされ、IllegalStateException 例外が生成される可能性があります。

以下の図は、バックプレッシャーの概略図です。バックプレッシャー コンポーネントは、上流からプッシュされたメッセージを継続的に受信します。バックプレッシャーの下流コンポーネントは、request(2) リクエストを開始し、次に 2 つのデータ要素を受信し、このプロセスを繰り返します。それでは、下流コンポーネントが request(2) リクエストを開始し続けない場合、データ要素 5 とデータ要素 6 をどのように処理すればよいでしょうか?

Error戦略の場合は例外がスローされ、Drop戦略の場合は再度request(2)が発行されるまで要素が破棄され、latest戦略の場合は最新のデータ要素がキャッシュされます(図中6番の要素)。 、ダウンストリームへのハンドオーバーを待機しています。バッファ戦略の場合、後続の要素は次の request(2) リクエストまでキャッシュされます。

非同期境界

リアクティブ ストリーミング標準では、コンポーネント間の分離を実現するために、データがコンポーネント間の非同期送信をサポートする必要があると提案しています。以下に示すように、Reactor の非同期境界は、スケジューラーとスイッチング メソッド、より具体的にはスレッドを通じて実装されます。

Flux.just(1, 2, 3)
  .log()
  .publishOn(Schedulers.parallel())    // 异步边界的产生
  .log()
  .map(o -> {
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return o * o;
  })
  .log()
  .filter(o -> o > 0)
  .log()
  .subscribe();
while (true) {}                     //防止主进程结束,看不到执行结果。
​
// 执行结果 此时数据处理是异步进行的
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
[ INFO] (main) | onSubscribe([Fuseable] FluxPublishOn.PublishOnConditionalSubscriber)
[ INFO] (main) | onSubscribe([Fuseable] FluxMapFuseable.MapFuseableConditionalSubscriber)
[ INFO] (main) | onSubscribe([Fuseable] FluxFilterFuseable.FilterFuseableSubscriber)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | request(256)
[ INFO] (main) | onNext(1)
[ INFO] (main) | onNext(2)
[ INFO] (parallel-1) | onNext(1)
[ INFO] (main) | onNext(3)
[ INFO] (main) | onComplete()       // 主进程结束数据发布
[ INFO] (parallel-1) | onNext(1)
[ INFO] (parallel-1) | onNext(1)
[ INFO] (parallel-1) | onNext(2)
[ INFO] (parallel-1) | onNext(4)
[ INFO] (parallel-1) | onNext(4)
[ INFO] (parallel-1) | onNext(3)
[ INFO] (parallel-1) | onNext(9)
[ INFO] (parallel-1) | onNext(9)
[ INFO] (parallel-1) | onComplete()
[ INFO] (parallel-1) | onComplete()
[ INFO] (parallel-1) | onComplete()

4. 業務用途でのご提案

リアクティブ プログラミングを適用する場合は、次の点に注意する必要があります。

  1. 非同期でノンブロッキングの操作: リアクティブ プログラミングの中核は非同期でノンブロッキングの操作であり、アプリケーションの同時実行性とパフォーマンスを向上させることができます。特定のビジネスでは、非同期 IO、非同期 HTTP クライアントなどのデータ フローを処理するために、非同期 API とノンブロッキング オペレーションを使用する必要があります。
  2. 信頼性と耐障害性: リアクティブ プログラミングでは、ネットワーク エラー、接続エラー、タイムアウトなど、一部のエラーと例外の処理を考慮する必要があります。したがって、例外処理、再試行メカニズムなどの適切なエラー処理メカニズムを使用する必要があります。
  3. バックプレッシャー メカニズム: バックプレッシャー メカニズムは、データ フローの安定性を確保するための重要なメカニズムです。データ ストリームのプロデューサーは速すぎてはいけません。そうしないと、コンシューマーが圧倒されてしまいます。バックプレッシャー メカニズムにより、プロデューサーの速度が制限され、コンシューマーが圧倒されるのを防ぐことができます。Reactive Streams のバックプレッシャーなどのバックプレッシャー フレームワーク、または Flowable または Observable に基づくバックプレッシャー フレームワークを使用できます。
  4. ベスト プラクティス: リアクティブ プログラミングを適用する際には、従うべきベスト プラクティスがいくつかあります。これには、副作用の回避、状態共有の回避、不変データ構造の使用などが含まれます。さらに、Thread.sleep() などのスレッドをブロックする操作は、リアクティブ プログラミングでは使用しないでください。
  5. 適切なフレームワークとライブラリを選択する: 特定のアプリケーションでは、適切なリアクティブ プログラミング フレームワークとライブラリを選択する必要があります。現在、Reactor、Akka、RxJava など、主流のリアクティブ プログラミング フレームワークやライブラリが多数存在します。適切なフレームワークとライブラリを選択すると、リアクティブ プログラミングをより適切に実行できるようになります。

要約すると、リアクティブ プログラミングを適用する場合は、リアクティブ プログラミングの中心原則とプログラミング モデルを完全に理解し、特定のビジネスと組み合わせて実践する必要があります。リアクティブ プログラミングは普遍的なプログラミング モデルではなく、特定のアプリケーション シナリオに向けたプログラミング モデルであることに注意してください。適用する場合は、シナリオと要件を合理的に評価する必要があります。

参考文献、書籍、リンク

1. Zheng Dewei「リアクティブストリームについて」

2. ProjectReactor を外部から学ぶ | Yanick のブログ

3. https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

4.リアクティブ プログラミングの分解 - Java 8、RxJava、Reactor の比較 - ナゲット

5.ソフトウェア開発 | レスポンシブプログラミングとレスポンシブシステム

6. (11) リアクティブ フロー仕様の詳細 - Reactive Spring Taoist Tool_Xiangxue IT のブログ - CSDN ブログ

おすすめ

転載: blog.csdn.net/xiaofeng10330111/article/details/130457788