Request Deduplication(リクエスト重複排除):概要、方法、動作メカニズム

著者: IT Sectr 公開日: 2026-06-13 読了時間: 9 分

Request Deduplication(リクエスト重複排除)は、同一の並列リクエストを1つにまとめるメカニズムであり、データソースは数十の代わりに1つの呼び出しのみを受け取ります。モバイルアプリケーションでは、重複排除が特に重要です。複数の画面が同時に同じユーザープロファイルや商品リストを要求する可能性があります。Square Engineering(2024年)によると、重複排除の導入により、サーバーロジックを変更することなくAPI負荷が30%削減されました。

重要なポイント

  • Request Deduplication(リクエスト重複排除) — 重複するリクエストを1つに統合し、結果をすべてのリクエスト元に送信する技術。
  • Memoization(メモ化) — 実行中にリクエスト結果をキャッシュする。後続の呼び出しは準備済みのオブジェクトを受け取る。
  • Request Merging(リクエストマージ) — 複数の異なるデータリクエストをサーバーへの1つのバッチリクエストに結合する。
  • DataLoader — GraphQLのライブラリで、サーバー上でバッチリクエスト重複排除を実装する。
  • ウィンドウタイムアウト — 送信前に重複リクエストのグループを収集するための短い遅延(10~50ミリ秒)。

リクエスト重複排除とは?

Request Deduplication(リクエスト重複排除)は、同じ時間枠内で同一のデータソースに対する複数の同一リクエストの実行を防ぐ技術です。10個の同一HTTPリクエストを送信する代わりに、システムは1つを送信し、残りの9つはその結果を待ちます。

重複リクエストの問題は、状態ベースのアーキテクチャ(MVVM、MVI、Redux)を使用するモバイルアプリケーションで特に深刻です。複数のオブザーバーが短時間に同じデータを購読すると、それぞれが独自のリクエストをトリガーし、冗長な負荷を生成します。Uber Engineering(2024年)によると、Uberのモバイルクライアントにおける全リクエストの最大18%が重複しており、クライアントサイドの重複排除によりその数は4分の1に削減されました。

重複排除はキャッシュと同じではありません。キャッシュは実行後にリクエスト結果を保存します。重複排除は、リクエストの実行前および実行中に冗長なリクエストを防ぎます。リクエストが完了した後は、キャッシュが機能します。

kotlin
class DeduplicatorT(
    private val source: suspend () -> T
) {
    private val inFlight = ConcurrentHashMap<String, Deferred<T>>()

    suspend fun get(key: String): T = inFlight.getOrPut(key) {
        async {
            source().also { inFlight.remove(key) }
        }
    }.await()
}

このKotlinクラスは、キーごとに1つのコルーチンのみが実行されることを保証します。同じキーを持つすべての同時呼び出しは、1つのDeferredを待機します。完了後、キーは削除され、次のリクエストは正常に実行されます。

モバイルアプリケーションで重複排除が必要な理由

サーバー負荷の軽減は、最初で最も明白な理由です。重複リクエストはそれぞれサーバーリソース(CPU、メモリ、データベース接続)を消費します。数百万台のデバイス規模では、10~15%の重複リクエストでも重大な負荷が生じ、追加のサーバーが必要になります。

バッテリーとデータ使用量の削減 — モバイルデバイス上の各HTTPリクエストは無線モジュールのエネルギーを消費します。Google I/O(2025年)によると、1つの失敗または重複リクエストは、1回のネットワークセッションのエネルギーの最大15%を消費する可能性があります。重複排除は無線モジュールの起動回数を減らし、デバイスのバッテリー寿命を延ばします。

データ競合の回避 — 2つの重複リクエストがローカルストレージにデータを書き込む場合、競合状態が発生する可能性があります。2番目のリクエストが最初の結果を古いデータで上書きする可能性があります。重複排除により、ローカルストレージへの書き込みが1回のみ行われ、競合が排除されます。

UXの向上 — ユーザーは同じデータに対して複数のローディングインジケーターを表示しません。UI状態(ローディング/成功/エラー)は、複数の競合するリクエストではなく、単一の信頼できる情報源によって管理されます。

Memoization(メモ化) — メモリ内キャッシュ

Memoization(メモ化)とは、関数の実行中にその結果をキャッシュすることです。関数が同じ引数で既に実行中の場合、新しい呼び出しは2番目のプロセスを開始せず、最初の結果を受け取ります。これはインプロセスシナリオにおける最も単純な重複排除の形式です。

モバイルアプリケーションでの典型的な実装は、DeferredまたはPromiseへのキーのHashMapです。キーは通常、リクエストURL文字列またはパラメータの連結です。エントリの有効期間は、最初のリクエストから応答が完了するまでです。Dropbox Engineering(2024年)によると、Dropboxモバイルクライアントでのメモ化により、重複APIリクエストが40%削減されました。

欠陥のある重複排除 — 危険な誤りです。エラー後にキーを削除しないと、後続のすべてのリクエストが永久に同じエラーを返します。正しい実装では、ErrorとFailureを処理し、キャッシュをクリアして再試行を許可する必要があります。

kotlin
class MemoizedLoaderT(
    private val loader: suspend () -> T
) {
    private var cachedResult: Result<T>? = null

    suspend fun get(): T = cachedResult ?.getOrThrow() ?: run {
        loader().let {
            Result.success(it)
        }.also { cachedResult = it }
    }.await()
}

MemoizedLoaderは、Result<T>を使用してエラーを正しく処理します。成功時はキャッシュし、エラー時は再試行を許可します。このアプローチにより、一時的なネットワーク障害が後続のリクエストをブロックしないことが保証されます。

Request Merging(リクエストマージ) — バッチ結合

Request Merging(リクエストマージ)は、同じソースへの複数の異なるリクエストをグループ化し、1つのバッチリクエストとして送信する技術です。重複排除とは異なり、ここでのリクエストは同一ではありません。パラメータは異なりますが、同じリソースを宛先とします。

典型的なシナリオ:アプリケーションの5つの画面が異なるユーザーのプロファイルを要求します。/api/users/1/api/users/2などへの5つの個別リクエストの代わりに、システムは20ミリ秒待機し、すべてのIDを収集して、1つのリクエスト/api/users?ids=1,2,3,4,5を送信します。ウィンドウタイムアウトが重要なパラメータです。長すぎるウィンドウはUXを損ない、短すぎると十分なリクエストを収集できません。

Netflix Engineering(2023年)によると、GraphQLアグリゲーターBFF(Backend for Frontend)では、リクエストマージによりレイヤー間のHTTP呼び出し数が65%削減され、余分なRTTを排除することで平均応答時間が120ミリ秒短縮されました。非同期ウィンドウ(デバウンス)は、コルーチンまたはRxJavaを介した標準的な実装です。

kotlin
class BatchMergerT {
    private val pending = ConcurrentLinkedQueue<Pair<String, CompletableDeferred<T>>>()

    suspend fun get(id: String): T = suspendCoroutine { cont ->
        pending.add(Pair(id, cont))
        scheduleFlush()
    }
}

このミックスインは、各リクエストを一時停止するためにsuspendCoroutineを、グループを収集するために30ミリ秒のウィンドウを使用します。タイマーが切れると、収集されたすべてのIDが1つのバッチリクエストで送信され、各コルーチンが結果を受け取ります。

DataLoaderによるサーバーサイド重複排除

DataLoaderは(元々はJavaScript/GraphQL用の)ライブラリで、サーバーサイドでバッチ処理とメモ化を実装します。1つのイベントループティック内で同じデータソースへのすべてのリクエストをグループ化し、1回の呼び出しで実行します。DataLoaderはGraphQLで広く使用されていますが、任意のRESTアプリケーションにも適用できます。

仕組み:1つのマイクロタスク内のすべてのloader.load(id)呼び出しがIDの配列に収集され、バッチ関数に渡されます。結果を受け取った後、各IDは配列要素を取得します。DataLoaderのキャッシュは1つのHTTPリクエスト内でのみ機能します。次のリクエストではキャッシュがクリアされ、データの新鮮さが保証されます。

Meta Engineering(2024年)によると、FacebookのGraphQLレイヤーへのDataLoaderの導入により、N+1問題が解消され、典型的なページあたりのデータベースクエリが200から10に削減されました。バッチスケジューリング — DataLoaderの主要な革新 — は、グループ化を最適化するためにprocess.nextTick(Node.js)またはDispatchQueue.main(iOS)を使用します。

どの重複排除戦略を選択するか

Memoization(メモ化)は、単一プロセス(モバイルアプリ、マイクロサービス)に最適です。実装が簡単で、同一の並列呼び出しに効果的です。欠点は、プロセス間やデバイス間では機能しないことです。

Request Merging(リクエストマージ)は、BFFレイヤーまたはアグリゲーターサービスに適しています。サーバー側でのバッチエンドポイントのサポートが必要です。フロントエンドが同じタイプの異なるデータに対して多数の小さなリクエストを行う場合に最適な選択です。

DataLoaderは、GraphQLサーバーの標準です。N+1問題を自動的に解決し、手動のキャッシュ設定を必要としません。GraphQLレイヤーを持つ任意のサーバーに推奨されます。

重複排除付きHTTPキャッシュ — OkHttp(Android)またはURLSession(iOS)レベルで、インターセプターまたはデリゲートを介して重複排除を設定できます。OkHttp CacheInterceptorは、同じURLのリクエストが既に実行中かどうかを確認し、それらをマージするカスタムインターセプターです。このメソッドはビジネスロジックレベルより下で動作し、機能コードを変更せずにアプリケーションのすべてのリクエストをカバーします。

よくある質問

重複排除とキャッシュの違いは何ですか?

重複排除は、最初のリクエストがまだ実行中に重複リクエストの実行を防ぎます。キャッシュは実行後に結果を保存します。これらは相互に補完します。重複排除はローディング中の繰り返しリクエストから保護し、キャッシュはその後の繰り返しリクエストから保護します。

重複排除が害を及ぼす可能性があるのはいつですか?

重複排除キーが誤って選択された場合です。例えば、すべてのユーザーが1つのキーを使用する場合、最初のリクエストが他のすべてをブロックします。キーは具体的でなければなりません:URL、パラメータ、ユーザーIDを含める必要があります。また、重複排除はメトリクス内の実際のリクエスト頻度を隠すことで、サーバーの問題を隠蔽する可能性もあります。

Request Mergingのウィンドウタイムアウトはどのように選択しますか?

ユーザー向けシナリオの最適なウィンドウは20~50ミリ秒です。これはリクエストのグループを収集するのに十分ですが、ユーザーが遅延に気づくほどではありません。バックグラウンド操作(ログ、分析)の場合、ウィンドウは200~500ミリ秒まで増やすことができます。経験則:ウィンドウは1つのリクエストの実行時間の10%を超えてはなりません。

重複排除はWebSocketでも機能しますか?

はい、同じ原則が適用されます。アプリの複数の部分が同じWebSocketチャンネルを購読する場合、重複排除器は1つの接続を開き、すべての購読者にメッセージを配信します。クライアントでのWebSocketメッセージの重複排除には、RxJava ShareまたはKotlin SharedFlowが理想的なツールです。

重複排除をテストするにはどうすればよいですか?

Androidの場合はMockWebServer(OkHttp)、iOSの場合はOHHTTPStubsを使用します。同一のパラメータで10個の並列リクエストを実行し、サーバーが正確に1回の呼び出しを受け取ったことを確認します。CountDownLatchまたはcoroutineScopeは、テスト内で並列呼び出しを同期するのに役立ちます。

まとめ

  • Request Deduplication(リクエスト重複排除) — 同一の並列リクエストを1つにマージし、結果をすべてのリクエスト元に配布する。
  • Memoization(メモ化) — 実行中に結果をキャッシュする。単一プロセス向けのシンプルで効果的な方法。
  • Request Merging(リクエストマージ) — 異なるリクエストのグループをバッチに収集する。サーバーのサポートとウィンドウタイムアウトが必要。
  • DataLoader — GraphQLの重複排除標準。サーバーレベルでN+1問題を解決する。
  • モバイルアプリの最大18%のリクエストが重複。重複排除によりサーバーとバッテリーの負荷を軽減。
  • 重複排除キーは具体的でなければならない:URL、パラメータ、ユーザーコンテキストを含める。
  • ベストプラクティス — クライアントサイド(OkHttp Interceptor / URLSession)とサーバーサイド(DataLoader)の重複排除の組み合わせ。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください