Interface 冪等性を保証の考え

date
Apr 18, 2024
slug
Idempotency
status
Published
tags
Interface architecture
summary
Interface 冪等性を保証の考え
type
Post
インターフェースの冪等性問題について説明する際、以下のフレーズを覚えておくと役立ちます。「一鍵、二判、三更新」。
この手順に従うことで、基本的に冪等性の問題を解決できます。
以下に、このフレーズの詳細を説明します:

一鍵(ロック)

まず、インターフェースにロックを追加する必要があります。これにより、他の人が操作中に介入することを防ぐことができます。このロックは、分散ロックでも悲観ロックでも構いません。重要なのは、ロックが排他的であること、つまり一度に一人だけが使用できることです。

二判(判定)

次に、操作の冪等性を判定します。判定方法は、状態機械、業務フローテーブル、あるいはデータベースの一意なインデックスに基づきます。簡単に言うと、すでに行われた操作かどうかを確認し、既に行われていた場合は再度実行しないということです。

三更新(更新)

最後に、データを更新します。操作の結果をデータベースに保存し、次回確認する際にその結果が確認できるようにします。
 
 

例1: Onlineで買い物時、重複コミットの対策

方法 1

  1. 注文ボタンをクリックした後、サーバーの応答を待つ間にボタンを灰色にし、再度クリックできないようにする。

利点

  • シンプル: 簡単に実装でき、基本的な重複送信を防止。
  • 負荷軽減: アクセスが多い場合、ブラウザ側で一部のリクエストをブロックし、バックエンドサーバーの負荷を軽減。

欠点

  • ページ操作には無効: ユーザーがページを前進・後退したり、リフレッシュ(F5キー)した場合、この方法では効果がない。
  • ネットワーク問題: リクエストが送信されなかったり、応答が得られなかったりする場合、再送信の可能性が残る。
  • 自動再試行: 多くのRPCフレームワークやゲートウェイには自動再試行機能があるため、フロントエンドだけで重複リクエストを完全に防ぐことは困難。

フロントエンドでの重複送信防止の効果

  • 価値あり: 完全ではないが、トラフィックをフィルタリングし、バックエンドサーバーの負荷を軽減することに役立つ。

処理 フロー

sequenceDiagram participant ユーザー participant フロントエンド as フロントエンド participant バックエンド as バックエンドサーバー ユーザー ->> フロントエンド: 注文ボタンをクリック フロントエンド ->> フロントエンド: ボタンを無効化(灰色に) フロントエンド ->> バックエンド: 注文リクエストを送信 バックエンド -->> フロントエンド: 応答を受信 フロントエンド ->> フロントエンド: ボタンを有効化 ユーザー ->> フロントエンド: 注文ボタンをクリック(応答なし)
 

方法2

  1. リクエストの一意なID + データベースの一意なインデックス制約

重複防止のステップ

  1. ユーザーが注文提出ページにアクセス:
      • フロントエンドはバックエンドの専用APIを呼び出し、一意のリクエストIDを生成。
  1. ユーザーが提出ボタンをクリック:
      • リクエストにリクエストIDを含む。
  1. リクエストIDの有効性確認:
      • サーバーはこのIDが既に使用されているかを確認。
      • 未使用ならリクエストを処理。
      • 使用済みなら重複提出の警告を表示。(一意Indexを利用)
  1. リクエストIDを保存:
      • システムのユニークなリストに保存し、重複提出を防ぐ。
 

注意点

  • この方法はシンプルで、基本的にはユーザーが意図せず複数回クリックすることによる重複提出問題を防ぐことができる。
  • しかし、高い同時実行数(例えば毎秒10万リクエスト)には耐えられない可能性がある。

処理フロー

sequenceDiagram participant ユーザー participant フロントエンド as フロントエンド participant バックエンド as バックエンドサーバー participant データベース as データベース ユーザー ->> フロントエンド: 注文提出ページにアクセス フロントエンド ->> バックエンド: 一意のリクエストIDを生成するAPIを呼び出し バックエンド ->> フロントエンド: 一意のリクエストIDを返す フロントエンド ->> ユーザー: リクエストIDをページに隠す ユーザー ->> フロントエンド: 提出ボタンをクリック フロントエンド ->> バックエンド: リクエストを送信 (リクエストIDを含む) バックエンド ->> データベース: リクエストIDの有効性を確認 alt リクエストIDが未使用 データベース -->> バックエンド: リクエストIDは未使用 バックエンド ->> データベース: リクエストIDを保存 バックエンド -->> フロントエンド: リクエストを処理 フロントエンド ->> ユーザー: 処理結果を表示 else リクエストIDが使用済み データベース -->> バックエンド: リクエストIDは使用済み バックエンド -->> フロントエンド: 重複提出の警告を表示 フロントエンド ->> ユーザー: 重複提出の警告を表示 end
 

方法3:Redis分布式ロック + リクエストの一意なID

方法2では、リクエストの一意なIDを使用し、データテーブルに一意のインデックスを追加する方法で重複注文を防止する方法を紹介しました。
Redisというキャッシュミドルウェアを導入し、データベースの負担を軽減する具体的な解決策を紹介します。

高トラフィックの問題

しかし、ビジネスが成長し、注文が増えるにつれて、1秒間に数十から数百、数千、さらには数万の注文リクエストが発生する場合、データベースがボトルネックとなり、システムの負担が大きくなります。
この問題を解決するために、Redisというキャッシュミドルウェアを導入し、データベースの負担を軽減する具体的な解決策を紹介します。

重複防止のステップ

  1. ユーザーが注文提出ページにアクセス
      • システムはバックエンドAPIを呼び出し、一意のリクエストIDを生成してRedisキャッシュに保存し、そのIDをフロントエンドに返します。フロントエンドはこのIDをページに埋め込みます。
  1. ユーザーが提出ボタンをクリック
      • バックエンドはRedis内にそのリクエスト一意IDが存在するかを確認します。
      • 存在しなければ、エラーメッセージを返します。
      • 存在すれば、次の検証プロセスに進みます。
  1. Redisの分散ロック機構を利用
      • リクエストIDを短期間ロックします。
      • ロック成功の場合、処理を続行します。
      • ロック失敗の場合、「注文処理中のため、重複提出を避けてください」というメッセージを返します。
  1. 処理完了後
      • Redis内のロックを解除し、処理済み注文のリクエストIDをクリアします。

データベースの一意インデックスについて

理論的には省略可能ですが、追加することでデータの一貫性を高め、潜在的なデータ衝突を防ぐことができます。
この方法は、拡張後に10万QPS(毎秒クエリ数)の高い同時実行数のシナリオに効率的に対応できます。
 
処理フロー
sequenceDiagram participant ユーザー participant フロントエンド as フロントエンド participant バックエンド as バックエンドサーバー participant Redis as Redisキャッシュ participant データベース as データベース ユーザー ->> フロントエンド: 注文提出ページにアクセス フロントエンド ->> バックエンド: 一意のリクエストID生成APIを呼び出し バックエンド ->> Redis: 一意のリクエストIDを保存 バックエンド ->> フロントエンド: 一意のリクエストIDを返す フロントエンド ->> ユーザー: リクエストIDをページに埋め込む ユーザー ->> フロントエンド: 提出ボタンをクリック フロントエンド ->> バックエンド: リクエストを送信 (リクエストIDを含む) バックエンド ->> Redis: リクエストIDの有効性を確認 alt リクエストIDが未使用 Redis -->> バックエンド: リクエストIDは未使用 バックエンド ->> Redis: リクエストIDをロック alt ロック成功 バックエンド ->> データベース: リクエストを処理 バックエンド ->> Redis: リクエストIDのロックを解除し、IDをクリア バックエンド -->> フロントエンド: 処理結果を返す フロントエンド ->> ユーザー: 処理結果を表示 else ロック失敗 バックエンド -->> フロントエンド: 重複提出の警告を返す フロントエンド ->> ユーザー: 重複提出の警告を表示 end else リクエストIDが使用済み Redis -->> バックエンド: リクエストIDは使用済み バックエンド -->> フロントエンド: エラーメッセージを返す フロントエンド ->> ユーザー: エラーメッセージを表示 end
 

案4:Redis分布式ロック + トークン

案3では、注文ごとにサーバーから一意のリクエストID(requestId)を取得する必要がありましたが、これは追加の手間となります。そこで、このステップを省略し、トークンを用いる方法を紹介します。

トークン生成の方法

ユーザーのリクエストに含まれるいくつかの重要な情報を組み合わせて、一意のトークンを生成します。このトークンはrequestIdの代わりに使用されます。例えば、次のような情報を組み合わせてトークンを生成します:
  • アプリケーション名
  • インターフェース名
  • メソッド名
  • リクエストパラメータの署名(リクエストヘッダー、ボディパラメータのSHA1値)
これにより、一意のトークンが生成され、サーバーからIDを取得する手間を省くことができます。

フロー図

  1. ユーザーが提出ボタンをクリック
      • サーバーがリクエストを受け取り、特定のルールに基づいて一意のトークンを生成します。
      • このトークンはリクエストIDの代わりとなります。
  1. トークンのロック試行
      • サーバーはRedisの分散ロック機能を用いて、このトークンに対してロックを試みます。
      • ロック成功の場合、注文処理を続行します。
      • ロック失敗の場合、「注文処理中のため、重複提出を避けてください」というメッセージを返します。
  1. 注文処理完了後
      • Redis内のロックを解除し、トークンをクリアします。
 

メリット

  • 追加の手間を省く: サーバーに追加のIDリクエストを送る手間を省き、注文処理の効率を向上。
  • インターフェーステストが容易: サーバー側でリクエストからトークンを生成することで、インターフェーステストが容易。
 
処理フロー
sequenceDiagram participant ユーザー participant フロントエンド as フロントエンド participant バックエンド as バックエンドサーバー participant Redis as Redisキャッシュ ユーザー ->> フロントエンド: 提出ボタンをクリック フロントエンド ->> バックエンド: リクエストを送信 バックエンド ->> バックエンド: 一意のトークンを生成 バックエンド ->> Redis: トークンのロック試行 alt ロック成功 Redis -->> バックエンド: ロック成功 バックエンド ->> バックエンド: 注文処理 バックエンド ->> Redis: ロック解除、トークンをクリア バックエンド -->> フロントエンド: 処理結果を返す フロントエンド ->> ユーザー: 処理結果を表示 else ロック失敗 Redis -->> バックエンド: ロック失敗 バックエンド -->> フロントエンド: 重複提出の警告を返す フロントエンド ->> ユーザー: 重複提出の警告を表示 end
記事に関する疑問があればお気軽にご連絡ください。