21
17

More than 5 years have passed since last update.

100万件データをサーバーから取得してブラウザに表示させるのにSpring WebFlux使ったら初期描画が速くなると思ったのでやってみた

Last updated at Posted at 2019-04-23

先日、「Spring WebFluxのFlux結果をJSからFetch APIで取得する」という記事を書きましたが、この方法を使って、
例えば、大量のデータをブラウザに表示させたい時に、サーバーで最初の数10件だけ処理できたら、その数10件だけ先にブラウザに表示させて、その後処理できたデータをバックグラウンドでちょろちょろ追加していく。みたいなことが簡単にできそうだなと思ったので試してみました。

ユーザーに大量のデータを要求された場合、ページングで実装するという選択肢もありますが、
こういう選択肢もあるかもねー。程度の記事です。この方法をオススメするわけではありません。

ここで試したものはGitHubにアップしています。

処理内容

100万件のデータをサーバーに要求して、結果をブラウザのグリッドに表示します。

使ってる技術要素

  • サーバーサイド
    • Java11
    • Spring WebFlux
    • H2 Database
      データベース&jdbc。ガチでDBサーバー立てるのめんどくさいのでH2でファイルDBにしました。
    • uroboroSQL
      DBアクセスライブラリ。弊社のOSS。何使っても良かったのですけど、私これに慣れてるのでこれ使います。
  • クライアントサイド
    • Vue.js
      何使っても良かったのですけど、私がVue好きなのと、Vue CLI使うと簡単に構築出来てとても好きなので使います。
    • Cheetah Grid
      グリッドライブラリ。自分が作った弊社のOSS。100万件表示できるように作ったつもりなのでこれ使います。

比較と動画

先に比較結果をお見せしておきます。

Spring WebFluxを利用した非同期リクエスト

最初に描画されるまでの時間は2,056msでした。
もう少し早いかなーと思ったのですが、H2 DatabaseでSQLクエリを発行してから1件目が返るまでに時間がかかるみたいですね。(まあH2はプロダクション用途ではないと思うので仕方ないです。)
動画は切ってしまっていますが、その後、チョロチョロデータを取得して、100万件をクライアント側に溜め込むまでに40秒ぐらいかかります。

WebFlux.gif

普通の同期リクエスト

描画されるまでの時間は5,827msでした。
一度に全部持ってきてるので、100万件をクライアント側に溜め込むまでの時間という意味でも同じ5,827msです。
もっと遅くなると思っていましたが、各レコードの構築で何か処理をしているわけでもなく、1レコードの情報も少ないので意外と速かったというところでしょうか?

sync.gif

実装

以下で実装についても書いておきます。

Controller(サーバーサイド)

以下のような実装になっています。

    @Autowired
    private SqlConfig uroboroSQLConfig;

    @Autowired
    @Qualifier("jdbcScheduler")
    private Scheduler jdbcScheduler;

    @GetMapping
    public Flux<Person> all() {
        return Flux.<Person> create(sink -> {
            try (var agent = uroboroSQLConfig.agent()) {
                // テーブルをSELECTしてPersonクラスにMappingしたStreamを生成
                agent.queryWith("SELECT * FROM PERSON ORDER BY ID")
                        .stream(Person.class)
                        // キャンセルされていたら終了させる
                        .dropWhile(p -> sink.isCancelled())
                        // FluxSinkにEntityを渡す
                        .forEach(sink::next);

                if (!sink.isCancelled()) {
                    // 終了を通知
                    sink.complete();
                }
            }
        }).subscribeOn(jdbcScheduler);
    }

GitHubに上げてあるソースだとこのあたりです。

uroboroSQLConfigというのはuroboroSQLのDBアクセスで必要なものです。本題とは関係ないので気にせず、DBアクセス用の何かなんだなーと思って空気読んでいただければと思います。

jdbcSchedulerReactorSchedulerインスタンスで、並列化に必要なものです。

適切なスレッド数でSchedulerを定義して、

        }).subscribeOn(jdbcScheduler);

と渡すことで、DBアクセスの処理と、リクエストを返す処理を並列化します。

あとは、キャンセル処理とかもしていますが、
基本的にはDBからSELECTしてその結果をsink.next(...)で渡していくだけです。

リクエスト(クライアントサイド)

Spring WebFluxのFlux結果をJSからFetch APIで取得するで書いた方法で、Fetch APIを使ってサーバーにリクエストします。

グリッドに表示(クライアントサイド)

以下のような実装になっています。

      const records = [];
      const grid = /* Cheetah Gridのインスタンス */;
      let buffer = [];
      //...
      streamJsonForVue(this, "/api/persons", {}, rec => {
        buffer.push(rec);
        if (
          buffer.length >= 10000 ||
          (records.length < 10000 && buffer.length >= 1000) ||
          (records.length < 1000 && buffer.length >= 100)
        ) {
          records.push(...buffer);
          grid.records = records;
          buffer = [];
          //...
        }
      }).then(() => {
        records.push(...buffer);
        grid.records = records;
        //...
      });

GitHubに上げてあるソースだとこのあたりです。

grid変数にCheetah Gridのインスタンスが入っていて、recordsプロパティに配列を与えると、その与えたデータが画面に表示されます。

streamJsonForVue関数は中で、「Spring WebFluxのFlux結果をJSからFetch APIで取得する」に書いた、Fetch APIの処理をしています。
プラスで、画面が破棄された時に、Fetchを中断するとかやってますが、本題には関係ないので説明は割愛します。

streamJsonForVueのcallbackから返ってきた、rec変数はControllerでsink.next(...)で渡された1レコードデータです。
1件ずつgridに渡してしまうと、再描画が走りまくってユーザーが画面を操作しにくくなるため、buffer配列に溜め込んで、ある程度溜まったら(最初は100件)、gridに反映させるようにしています。

同期リクエストの方の実装

GitHubに上げてあるソースだとこのあたりこのあたりです。
比較用に作ってみただけで、本題には関係ないので説明は割愛します。

実装した感想

私が実装してみて思ったこととしては、実装がとても簡単だったということです。
自分の知識だと、HTTPレスポンスを非同期でStreamに流すのは難しいイメージでしたが、Spring WebFlux使うととても簡単に出来たのでびっくりしています。

結果

先にも書きましたが、今回のサンプルでは、初期描画にかかる時間はそれぞれ以下の結果になりました。

  • Spring WebFluxを利用した非同期リクエスト
    2,056ms
  • 普通の同期リクエスト
    5,827ms

今回のサンプルでは、思っていたより効果が出なくて残念でしたが、どうしても初期描画だけは速くしたいような場合に、もしかしたら使えるかもしれません(?)

21
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
17