記事ディレクトリ
序文:
インターフェイスの冪等性の問題は、言語とは関係なく、開発者にとって公的な問題です。この記事では、この種の問題を解決するための非常に実用的な方法をいくつか紹介します。そのほとんどは私がプロジェクトで実装したもので、困っている友人の参考として使用できます。
あなたが次のようなシナリオに遭遇したかどうかはわかりません。
1.フォームに入力するときに、誤って保存ボタンを素早く 2 回クリックしてしまい、テーブル内に 2 つの重複データが生成されますが、ID は異なります。
2.私たちのプロジェクトにおけるインターフェースのタイムアウト問題を解決するために、通常、再試行メカニズムを導入します。インターフェースへの最初のリクエストがタイムアウトになり、リクエスタは応答結果を時間内に取得できませんでした(この時点では成功しているかもしれません)が、誤った結果が返されることを避けるため(この状況では直接失敗を返すことはできませんよね?) 、リクエストは数回再試行され、重複データも生成されます。
3. mq コンシューマーはメッセージを読み取るときに重複メッセージを読み取る場合があり、それらがうまく処理されないと重複データも生成されます。はい、これらはべき等の問題です。
インターフェイスの冪等性とは、同じ操作に対してユーザーが開始した 1 つまたは複数のリクエストの結果に一貫性があり、複数のクリックによって引き起こされる副作用がないことを意味します。
このタイプの問題は主にインターフェイスで発生します。
挿入操作、この場合複数のリクエストでは重複データが生成される可能性があります。
update user set status=1 where id=1のように、更新操作が単純にデータを更新する場合は、問題はありません。update user set status=status+1 where id=1などの計算がある場合、この場合の複数のリクエストによりデータ エラーが発生する可能性があります。
では、インターフェイスの冪等性をどのように確認すればよいでしょうか?
1.挿入する前に選択します
通常、データを保存するインターフェースでは、データの重複を防ぐために、名前またはコードフィールドに基づいてデータを選択してから挿入します。データがすでに存在する場合は更新操作が実行され、データが存在しない場合は 挿入操作が実行されます。
このソリューションは、データの重複を防ぐために通常最もよく使用されるソリューションです。ただし、このソリューションは同時シナリオには適していません。同時シナリオでは、他のソリューションと組み合わせて使用する必要があり、そうしないと重複データも生成されます。誰かがトラブルに巻き込まれないように、ここで言及します。
2.悲観的ロックを追加する
支払いシナリオでは、ユーザー A の口座残高が 150 元で、100 元を送金したいと考えていますが、通常の状況では、ユーザー A の残高はわずか 50 元です。一般的に SQL は次のようになります。
update user amount = amount-100 where id=123;
同じリクエストが複数回発生すると、ユーザー A の残高がマイナスになる可能性があります。この場合、ユーザAは泣く可能性がある。同時に、これは非常に重大なシステムのバグであるため、システム開発者も泣くかもしれません。
この問題を解決するには、悲観的ロックを追加してユーザー A のデータ行をロックします。ロックの取得とデータの更新を同時に許可されるリクエストは 1 つだけであり、他のリクエストは待機します。
通常、単一行のデータは次の SQL によってロックされます。
select * from user id=123 for update;
具体的なプロセスは次のとおりです。
具体的な手順:
- 複数のリクエストは、ID に基づいてユーザー情報を同時に照会します。
- 残高が100未満かどうかを判断し、残高が不足している場合は不足残高を直接返却します。
- 残高が十分な場合は、更新のためにユーザー情報を再度照会し、ロックの取得を試みます。
- 最初のリクエストのみが行ロックを取得でき、ロックを取得していない残りのリクエストは、ロックを取得する次の機会を待ちます。
- 最初のリクエストはロックを取得した後、残高が 100 未満かどうかを判断します。残高が十分な場合は、更新操作が実行されます。
- 残高が不足している場合は、リクエストが繰り返され、成功が直接返されることを意味します。
次の点に特別な注意を払う必要があります。 mysql データベースを使用している場合、ストレージ エンジンはトランザクションのみをサポートするため、innodb を使用する必要があります。さらに、ここでの id フィールドは主キーまたは一意のインデックスである必要があります。そうでない場合は、テーブル全体がロックされます。
悲観的ロックでは、同じトランザクション操作中にデータ行をロックする必要があるため、トランザクションに時間がかかると、大量のリクエストが待機し、インターフェイスのパフォーマンスに影響を与えます。また、各リクエスト インターフェイスで同じ戻り値を保証することは難しいため、冪等設計シナリオには適していませんが、アンチヘビー シナリオでは使用できます。ちなみに、重複防止設計 と 冪等設計には実は違いがあります。重複防止設計は主にデータの重複を避けることを目的としており、インターフェイスの戻りに対する要件はそれほど多くありません。べき等設計では、データの重複を回避するだけでなく、各リクエストが同じ結果を返すことも必要です。
3. 楽観的ロックを追加する
悲観的ロックにはパフォーマンスの問題があるため、インターフェイスのパフォーマンスを向上させるために、楽観的ロックを使用できます。タイムスタンプまたはバージョンフィールドをテーブルに追加する必要があります。ここでは例としてバージョンフィールドを取り上げます。
データを更新する前にデータをクエリします。
select id,amount,version from user id=123;
データが存在する場合は、見つかったバージョンが1に等しいと仮定して、idフィールドとversionフィールドをクエリ条件として使用してデータを更新します。
update user set amount=amount+100,version=version+1where id=123 and version=1;
データの更新中にバージョン + 1が追加され、この更新操作によって影響を受ける行数が決定されます。値が 0 より大きい場合は、更新が成功したことを意味します。値が 0 に等しい場合は、更新操作が成功したことを意味します。アップデートによってデータは変更されませんでした。
バージョン1に対する最初のリクエストは成功する可能性があるため、操作が成功すると、バージョンは2になります。このとき、同時リクエストが来た場合は、同じ SQL を再度実行します。
update user set amount=amount+100,version=version+1where id=123 and version=1;
この更新操作では実際にデータが更新されるわけではなく、バージョンが2になっているため、最終的に SQL 実行結果に影響を受ける行数は0となり、whereのversion=1は確実に条件を満たしません。ただし、インターフェイスの冪等性を確保するために、インターフェイスは直接成功を返すことができます。バージョン値が変更されているため、前のリクエストは 1 回成功する必要があり、後続のリクエストは繰り返されます。
具体的なプロセスは次のとおりです。
具体的な手順:
- まず、バージョン フィールドを含む ID に基づいてユーザー情報をクエリします。
- where条件のパラメータとしてのidとversionフィールドの値に従って、ユーザー情報が更新され、version+1
- 操作によって影響を受ける行数を確認します。影響する行が 1 行であれば、それはリクエストであり、他のデータ操作を実行できることを意味します。
- 影響を受ける行が 0 行の場合、リクエストが繰り返され、成功が直接返されることを意味します。
4.一意のインデックスを追加する
ほとんどの場合、重複データの生成を防ぐために、テーブルに一意のインデックスを追加しますが、これは非常にシンプルで効果的な解決策です。
alter table `order` add UNIQUE KEY `un_code` (`code`);
一意のインデックスを追加すると、最初のデータ要求を正常に挿入できるようになります。ただし、後続の同一のリクエストでは、データの挿入時にキー 'order.un_code の重複エントリ '002'例外が報告され、一意のインデックスが競合していることを示します。
例外をスローしてもデータには影響しませんが、データに誤りが生じることはありません。ただし、インターフェイスの冪等性を保証するには、例外をキャッチして成功を返す必要があります。
Javaプログラムの場合は、 DuplicateKeyException例外をキャッチする必要があり、 Springフレームワークを使用している場合は、MySQLIntegrityConstraintViolationException例外もキャッチする必要があります。
具体的なフローチャートは以下の通りです。
具体的な手順:
- ユーザーがブラウザを通じてリクエストを開始すると、サーバーがデータを収集します。
- そのデータをmysqlに挿入します
- 実行が成功したかどうかを判断し、成功した場合は、他のデータ (場合によっては他のビジネス ロジック) を操作します。実行が失敗した場合は、一意のインデックスの競合例外が捕捉され、成功が直接返されます。
5. 守備のスケジュールを立てる
テーブル内のすべてのシナリオで重複データが許可されるわけではなく、特定のシナリオのみが許可される場合があります。現時点では、一意のインデックスをテーブルに直接追加するのは明らかに適切ではありません。
この状況に対応するには、重さ対策テーブルを構築することで問題を解決できます。
テーブルに含めることができるのは 2 つのフィールドのみです: id と 一意のインデックス。一意のインデックスには、名前、コードなどの複数のフィールドと組み合わせた一意の識別子を指定できます (例: susan_0001)。
具体的なフローチャートは以下の通りです。
具体的な手順:
- ユーザーがブラウザを通じてリクエストを開始すると、サーバーがデータを収集します。
- データを mysql アンチヘビー テーブルに挿入します。
- 実行が成功したかどうかを判断し、成功した場合は、mysql で他のデータ操作 (場合によっては他のビジネス ロジック) を実行します。
- 実行が失敗した場合は、一意のインデックスの競合例外が捕捉され、成功が直接返されます。
次の点に特別な注意を払う必要があります。重複防止テーブルとビジネス テーブルは同じデータベース内に存在する必要があり、操作は同じトランザクション内に存在する必要があります。
6. ステートマシンに従って
多くの場合、ビジネス テーブルにはステータスがあります。たとえば、注文テーブルには、1-注文、2-支払い、3-完了、4-キャンセルなどのステータスがあります。これらの状態の値が規則的で、ビジネス ノードがたまたま小さいものから大きいものまである場合、それを使用してインターフェイスの冪等性を確認できます。
id=123 の注文ステータスが支払い済であれば完了となります。
update `order` set status=3 where id=123 and status=2;
最初のリクエスト時は注文ステータスが決済済みで値が2なので、update文は正常にデータを更新できますが、SQL 実行結果の影響を受ける行数は1となり、注文ステータスは3になります。
その後同じリクエストが来て、再度同じSQLを実行すると、注文ステータスが3になり、status=2が条件となるため、更新する必要のあるデータをクエリできなくなり、影響を受ける行数が減少します。最終的な SQL 実行結果は0です。つまり、データは実際には更新されません。ただし、インターフェイスの冪等性を保証するために、影響を受ける行の数が0の場合、インターフェイスは直接成功を返すこともできます。
具体的なフローチャートは以下の通りです。
具体的な手順:
- ユーザーがブラウザを通じてリクエストを開始すると、サーバーがデータを収集します。
- IDと現在の状態を条件として次の状態に更新します
- 操作の影響を受ける行数を確認します。1 行が影響を受ける場合、現在の操作は成功し、他のデータ操作を実行できます。
- 影響を受ける行が 0 行の場合、リクエストが繰り返され、成功が直接返されることを意味します。
注意すべき主な点は、この解決策は、更新されるテーブルにステータス フィールドがあり、そのステータス フィールドを更新する必要があるという特殊なケースに限定されており、すべてのシナリオに適用できるわけではないということです。
7. 分散ロックを追加する
実際、前に紹介した一意のインデックスの追加や重複防止テーブルの追加は、基本的に、分散ロックの一種であるデータベースの分散ロックを使用しています。ただし、データベース分散ロックのパフォーマンスはあまり良くないため、代わりにredisまたはZookeeperを使用できます。
多くの企業の分散構成センターでは、zookeeperの代わりにapolloまたはnacosが使用されているため、分散ロックを導入する例としてredisを使用します。
現在、Redis 分散ロックを実装するには主に 3 つの方法があります。
- setNx コマンド
- コマンドを設定する
- 再検討フレームワーク
それぞれのオプションには独自の長所と短所があり、関連記事が多すぎるため、ここでは詳しく説明しません。
具体的なフローチャートは以下の通りです。
具体的な手順:
- ユーザーがブラウザを通じてリクエストを開始すると、サーバーがデータを収集し、唯一のビジネス フィールドとして注文番号コードを生成します。
- redis set コマンドを使用して、注文コードを redis に設定し、同時にタイムアウトを設定します。
- 設定が成功したかどうかを確認し、設定が成功した場合は、最初のリクエストであることを意味し、データ操作が実行されます。
- 設定が失敗した場合は、リクエストが繰り返されたことを意味し、成功が直接返されます。
次の点に特別な注意を払う必要があります: 分散ロックは適切な有効期限を設定する必要があります。設定が短すぎると、反復的なリクエストを効果的に防止できません。設定が長すぎると、 redis のストレージ領域が無駄になる可能性があるため、実際のビジネスの状況に応じて決定する必要があります。
8. トークンの取得
上記のスキームに加えて、トークンを使用する最後のスキームがあります。このソリューションはこれまでのすべてのソリューションとは少し異なり、ビジネス操作を完了するには 2 つのリクエストが必要です。
- トークンを取得するための最初のリクエスト
- 2 番目のリクエストは、ビジネス操作を完了するためにこのトークンを運びます。
具体的なフローチャートは以下の通りです。
最初のステップは、まずトークンを取得することです。
2 番目のステップは、特定のビジネス操作を実行することです。
具体的な手順:
- ユーザーがページにアクセスすると、ブラウザは自動的にトークン リクエストを開始します。
- サーバーはトークンを生成し、それを redis に保存し、ブラウザーに返します。
- トークンは、ユーザーがブラウザを通じてリクエストを開始するときに送信されます。
- トークンが Redis に存在するかどうかをクエリします。存在しない場合は、それが最初のリクエストであることを意味し、後続のデータ操作を実行します。
- 存在する場合は、リクエストが繰り返されたことを意味し、直接成功を返します。
- Redis では、トークンは有効期限が過ぎると自動的に削除されます。
上記のスキームは冪等性を考慮して設計されています。
反重量設計の場合は、フローチャートを変更する必要があります。