Redis 分散ロック |ブロンズからダイヤモンドまでの5つの進化スキーム

Redis 分散ロック |ブロンズからダイヤモンドまでの5つの進化スキーム

[[396901]]

前回の記事では、システムパフォーマンスを向上させるためのキャッシュを行うローカルメモリの使い方「キャッシュ実践編(その1)」について説明しました。また、キャッシュの故障の問題を解決するためにロックを使用する方法についても説明しました。ただし、ローカル ロック方式は分散シナリオには適用できないため、この記事では、ローカル ロックの問題を解決するために分散ロックを導入する方法について説明します。この記事のすべてのコードとビジネスは、私のオープンソース プロジェクト PassJava に基づいています。

この記事の主な内容は次のとおりです。

1. ローカルロックの問題

まず、ローカル ロックの問題を確認しましょう。

現在、トピックのマイクロサービスは 4 つのマイクロサービスに分割されています。フロントエンドのリクエストが届くと、さまざまなマイクロサービスに転送されます。フロントエンドが 100,000 件のリクエストを受信し、各マイクロサービスが 25,000 件のリクエストを受信した場合、キャッシュに障害が発生すると、各マイクロサービスはデータベースにアクセスするときにロックし、キャッシュの破損を防ぐためにロック (同期またはロック) を通じて独自のスレッド リソースをロックします。

これはローカル ロック方式であり、分散環境ではデータの不整合が発生する可能性があります。たとえば、サービス A がデータを取得した後、キャッシュ キー = 100 を更新します。サービス B はサービス A のロックによって制限されず、キャッシュ キー = 99 に更新を送信します。最終結果は 99 または 100 になる可能性がありますが、これは不明な状態であり、予想される結果と一致しません。フローチャートは次のとおりです。

2. 分散ロックとは何か

上記のローカル ロックの問題に基づいて、分散クラスター環境をサポートするロックが必要です。DB をクエリする場合、1 つのスレッドだけがアクセスでき、他のスレッドは最初のスレッドがロック リソースを解放するまで待機してから実行を続行する必要があります。

人生における例: ロックをドアの外にある鍵と考え、すべての同時スレッドを人として考えることができます。全員が部屋に入りたいのですが、部屋に入ることができるのは 1 人だけです。誰かが入ってくるとドアは施錠され、その人が出てくるまで他の人は待たなければなりません。

次の図に示すように、分散ロックの基本原理を見てみましょう。

上の図の分散ロックを分析してみましょう。

  • 1. フロントエンドは、10W の高同時実行リクエストを 4 つのトピック マイクロサービスに転送します。
  • 2. 各マイクロサービスは 2.5W のリクエストを処理します。
  • 3. リクエストを処理する各スレッドは、ビジネスを実行する前にロックを取得する必要があります。 「穴を占拠する」と理解できます。
  • 4. ロックを取得したスレッドは、処理を完了するとロックを解除します。 「ピットスペースを解放する」と解釈できます。
  • 5. ロックを取得していないスレッドは、ロックが解放されるまで待機する必要があります。
  • 6. ロックを解放した後、他のスレッドがロックを取得します。
  • 7. 手順 4、5、6 を繰り返します。

簡単な説明: すべてのリクエスト スレッドは、「スロットを占有」するために同じ場所に移動します。スロットがある場合は、ビジネス ロジックが実行されます。スロットがない場合、他のスレッドが「スロット」を解放する必要があります。このピットはすべてのスレッドに表示されます。このピットを Redis キャッシュまたはデータベースに配置できます。この記事では、Redis を「分散ピット」として使用する方法について説明します。

3. Redis SETNX

Redis は、誰でもアクセス可能な場所であるため、「ピットを占領する」場所として使用できます。

Redis を使用して分散ロックを実装するためのソリューションはいくつかあります。私たちは皆、SETNX コマンド (キーを特定の値に設定する) を使用します。ただ、高レベルソリューションによって渡されるパラメータの数が異なり、異常な状況が考慮されるだけです。

このコマンドを見てみましょう。 SETNX は、存在しない場合に set する略語です。つまり、キーが存在しない場合にはキーの値を設定し、存在する場合には何もしません。

Redis コマンドラインでの実行方法は次のとおりです。

  1. <キー> <値> NXを設定する

redis コンテナに入り、SETNX コマンドを試すことができます。

まずコンテナを入力します:

  1. docker exec -it <コンテナID> redid-cli

次に、SETNX コマンドを実行します。キー wukong に対応する値を 1111 に設定します。

  1. セットウーコン 1111 NX

設定が成功したことを示す OK を返します。コマンドを繰り返します。 nil が返された場合は設定が失敗したことを意味します。

4. ブロンズプラン

まず、Redis の SETNX コマンドを使用して、最も単純な分散ロックを実装します。

3.1 ブロンズ原則

フローチャートを見てみましょう:

  • 複数の同時スレッドが Redis のロックに適用され、setnx コマンドが実行されます。スレッド A が正常に実行されたと仮定すると、現在のスレッド A がそれを取得したことを意味します。
  • 他のスレッドは setnx コマンドの実行に失敗するため、スレッド A がロックを解放するまで待機する必要があります。
  • スレッド A は自身の処理の実行を終えると、ロックを削除します。
  • 他のスレッドは引き続きロックを取得し、setnx コマンドを実行します。スレッド A がロックを削除したため、他のスレッドがロックを取得できるようになります。

コード例は以下のとおりです。 Java の setnx コマンドに対応するコードは setIfAbsent です。

setIfAbsent メソッドの最初のパラメーターはキーを表し、2 番目のパラメーターは値を表します。

  1. // 1. 最初にロックを奪取する
  2. ブール値 lock = redisTemplate.opsForValue().setIfAbsent( "lock" , "123" );
  3. if(ロック) {
  4. // 2. ビジネスを成功裏に獲得し実行する
  5. リスト<TypeEntity> typeEntityListFromDb = getDataFromDB();
  6. // 3. ロック解除
  7. redisTemplate.delete ( "ロック" ) ;
  8. typeEntityListFromDbを返します
  9. }それ以外{
  10. // 4. しばらく寝る
  11. 睡眠(100);
  12. // 5. プリエンプションに失敗しました。ロックの解放を待機しています
  13. getTypeEntityListByRedisDistributedLock()を返します
  14. }

ちょっとした質問ですが、なぜしばらく眠る必要があるのでしょうか?

プログラムに再帰呼び出しが含まれているため、スタック スペース オーバーフローが発生する可能性があります。

3.2 ブロンズソリューションの欠点

青銅は最も基本的なものであり、間違いなく多くの問題をもたらすため、青銅と呼ばれています。

ある家族のシナリオを想像してみてください。夜、小空がドアの鍵を開けて一人で部屋に入り、電気をつけたら、突然電気が消えてしまいます。小空はドアを開けて外に出たいのですが、ドアの鍵が見つかりません。そうすると、シャオミンは入れなくなり、外にいる人も入れなくなります。

技術的な観点から見ると、setnx はロックを正常に取得しますが、ビジネス コードで例外が発生したり、サーバーがクラッシュしたりして、ロックを削除するロジックが実行されず、デッドロックが発生します。

では、このリスクを回避するにはどうすればよいでしょうか?

ロックの自動有効期限を設定します。一定時間が経過すると、ロックは自動的に削除され、他のスレッドがロックを取得できるようになります。

4. シルバープラン

4.1 実生活からの例

上記のブロンズ ソリューションにはデッドロックの問題があるため、上記のリスク回避ソリューションを使用して設計します。これがシルバー ソリューションです。

現実生活から別の例を見てみましょう。Xiaokong はドアのロックを解除した後、スマートロックに砂時計のカウントダウンを設定しました。砂時計が終わると、ドアのロックが自動的に開きました。突然部屋の電気が消えても、一定時間後に自動的に鍵が開き、他の人が入室できるようになります。

4.2 技術概略図

ブロンズ ソリューションとの違いは、ロックが正常に占有された後に、ロックの有効期限が設定される点です。これら 2 つのステップは段階的に実行されます。次の図に示すように:

4.3 サンプルコード

Redisキーを消去するためのコードは次のとおりです。

  1. // 10秒後に自動的にロックを解除します
  2. redisTemplate.expire( "ロック" 、10、TimeUnit.SECONDS);

完全なコードは次のとおりです。

  1. // 1. 最初にロックを奪取する
  2. ブール値 lock = redisTemplate.opsForValue().setIfAbsent( "lock" , "123" );
  3. if(ロック) {
  4. // 2. 10秒後にロックを自動的にクリーンアップする
  5. redisTemplate.expire( "ロック" 、10、TimeUnit.SECONDS);
  6. // 3. ビジネスを成功に導き実行する
  7. リスト<TypeEntity> typeEntityListFromDb = getDataFromDB();
  8. // 4. ロック解除
  9. redisTemplate.delete ( "ロック" ) ;
  10. 戻り値 typeEntityListFromDb;
  11. }

4.4 シルバーソリューションの欠点

Silver ソリューションは、スレッド例外またはサーバー クラッシュによりロックが解放されないという問題を解決しているように見えますが、まだ他の問題が残っています。

ロックの占有と有効期限の設定は 2 つのステップで実行されるため、この 2 つのステップの間に例外が発生すると、ロックの有効期限は正常に設定されません。

したがって、ブロンズ ソリューションと同じ問題、つまりロックの有効期限が切れないという問題があります。

5. 黄金の計画

5.1 アトミック命令

上記のシルバー ソリューションでは、ロックの占有とロックの有効期限の設定が 2 つのステップで実行されます。この時点で、トランザクションのアトミック性について考えることができます。

原子性: 複数のコマンドが正常に実行されるか、まったく実行されないかのいずれかになります。

ロックの占有 + ロックの有効期限の設定という 2 つのステップを 1 つのステップで実行します。

Redis はまさにこの操作をサポートしています:

  1. #キーの値を設定し、有効期限をミリ秒または秒単位で設定します。
  2. <キー> <値> PX <ミリ秒数> NXを設定します
  3. または
  4. <キー> <値> EX <秒数> NX を設定します

次のコマンドでキーの変更を確認できます。

  1. ttl <キー>

以下は、キーを設定し、有効期限を設定する方法を示しています。注意: コマンドを実行する前にキーを削除する必要があります。クライアントまたはコマンドを通じて削除できます。

  1. #キー= wukong、値 = 1111、有効期限 = 5000ms を設定します
  2. セットウーコン 1111 PX 5000 NX
  3. #キーのステータスを確認する
  4. ttl ウーコン

実行結果は下の図のようになります。ttl コマンドを実行するたびに、wukong の有効期限が短くなるのがわかります。最終的には-2(期限切れ)になります。

5.2 技術概略図

ゴールドプランとシルバープランの違い: ロックを取得する際に、ロックの有効期限も設定する必要があります。これは、正常に実行されるか、実行されないかのいずれかになるアトミック操作です。次の図に示すように:

5.3 サンプルコード

ロック値を 123 に設定し、有効期限を 10 秒に設定します。 10 秒経過してもロックがまだ存在する場合、ロックは解除されます。

  1. setIfAbsent( "ロック" , "123" , 10, TimeUnit.SECONDS);

5.4 黄金の解決策の欠点

現実の例を取り上げ、黄金の計画の欠陥を見てみましょう。

5.4.1 ユーザーAがロックを奪取する

  • ユーザー A は最初にロックをつかみ、10 秒後に自動的にロックが解除されるように設定します。ロック番号は123です。
  • 10秒後、Aはまだ作業を続けており、ロックは自動的に開きました。

5.4.2 ユーザーBがロックを奪取する

  • ユーザー B は部屋の鍵が開いていることに気づき、鍵をつかんでロック番号を 123 に設定し、有効期限を 10 秒に設定します。
  • 部屋内でタスクを実行できるのは 1 人のユーザーのみであるため、タスクの実行時にユーザー A とユーザー B の間で競合が発生します。
  • ユーザー A は 15 秒後にタスクを完了しますが、ユーザー B はまだタスクを実行しています。
  • ユーザー A は、番号 123 のロックをアクティブに開きます。
  • ユーザー B はまだタスクを実行しており、ロックが開いていることに気付きます。
  • ユーザー B は非常に怒っています: まだタスクを完了していないのに、なぜロックが開いているのですか?

5.4.3 ユーザーCがロックを奪取する

  • ユーザー B のロックが A によって開かれた後、B がまだタスクを実行している間に A は部屋を離れます。
  • ユーザー C がロックを取得し、タスクの実行を開始します。
  • 部屋内でタスクを実行できるのは 1 人のユーザーのみであるため、タスクの実行時にユーザー B とユーザー C の間で競合が発生します。

上記のケースから、ユーザー A がタスクを処理するのに必要な時間がロックが自動的にクリア (ロック解除) される時間よりも長いため、ロックが自動的にロック解除された後に他のユーザーがロックを奪取したことがわかります。ユーザー A がタスクを完了すると、他のユーザーが押収したロックを積極的に解除します。

なぜここで他人の鍵が開けられるのでしょうか?ロック番号はすべて「123」と呼ばれ、ユーザーAだけがロック番号を認識します。 「123」という番号の鍵を見つけると、彼はそれを開けます。その結果、ユーザー B のロックが解除されます。この時点で、ユーザーBはタスクを完了していないので、当然怒っています。

6. プラチナプラン

6.1 実生活からの例

上記の黄金比の解決策の欠点も簡単に解決できます。ロックごとに異なる番号を設定するだけです。

下の図に示すように、B が押収したロックは青色で、A が押収した緑色のロックとは異なります。このため、A では開けられません。

理解しやすいように動的な図を作成しました:

アニメーションデモ

静止画はより高解像度なので、ぜひご覧ください:

6.2 技術概略図

ゴールドプランとの違い:

  • ロックの有効期限を設定するときは、一意の番号も設定する必要があります。
  • ロックをアクティブに削除する場合は、ロック番号が設定と一致しているかどうかを判断する必要があります。一貫性がある場合は、設定したロックであると見なされ、アクティブに削除できます。

6.3 コード例

  1. // 1. 一意のIDを生成する
  2. 文字列 uuid = UUID.randomUUID().toString();
  3. // 2. ロックを奪取する
  4. ブール値 lock = redisTemplate.opsForValue().setIfAbsent( "lock" , uuid, 10, TimeUnit.SECONDS);
  5. if(ロック) {
  6. システム。 out .println( "押収成功: " + uuid);
  7. // 3. ビジネスを成功に導き実行する
  8. リスト<TypeEntity> typeEntityListFromDb = getDataFromDB();
  9. // 4. 現在のロック値を取得する
  10. 文字列 lockValue = redisTemplate.opsForValue().get( "lock" );
  11. // 5. ロック値が設定値と等しい場合は、独自のロックをクリーンアップします
  12. uuidがlockValueと等しい場合
  13. システム。 out .println( "クリーンロック: " + lockValue);
  14. redisTemplate.delete ( "ロック" ) ;
  15. }
  16. typeEntityListFromDbを返します
  17. }それ以外{
  18. システム。 out .println( "プリエンプションに失敗しました。ロックの解放を待機しています" );
  19. // 4. しばらく寝る
  20. 睡眠(100);
  21. // 5. プリエンプションに失敗しました。ロックの解放を待機しています
  22. getTypeEntityListByRedisDistributedLock()を返します
  23. }
  • 1. ランダムな一意の ID を生成し、ロックに一意の値を追加します。
  • 2. ロックを押収し、有効期限を 10 秒に設定すると、ロックにランダムな一意の ID が付与されます。
  • 3. チャンスをうまく捉えてビジネスを遂行する。
  • 4. ビジネスを実行した後、現在のロックの値を取得します。
  • 5. ロック値が設定値と等しい場合は、自身のロックをクリーンアップします。

6.4 プラチナソリューションの欠点

上記の解決策は完璧に思えますが、まだ問題が残っています。ステップ 4 と 5 はアトミックではありません。

  • 時間: 0秒。スレッドAがロックを取得します。
  • 時間: 9.5秒。スレッド A は Redis に現在のキーの値を照会します。
  • 時間: 10秒。ロックは自動的に期限切れになります。
  • 時間: 11秒。スレッド B がロックを取得します。
  • 時間: 12秒。スレッド A はクエリに長い時間がかかり、最終的に複数のロック値を取得します。
  • 時間: 13秒。スレッド A は、設定したロック値と返された値を比較し続けます。値が等しいので、ロックは解除されます。ただし、このロックは実際にはスレッド B によって取得されたロックです。

では、このリスクを回避するにはどうすればよいでしょうか?ダイヤモンドソリューションの登場です。

7. ダイヤモンドプラン

上記のスレッド A のロック照会および削除のロジックはアトミックではないため、ロック照会および削除はアトミック命令として操作できます。

7.1 技術概略図

下の図のように、赤丸で囲った部分がダイヤモンド解の差分です。アトミック操作を実現するには、スクリプトを使用して削除を実行します。

7.2 コード例

スクリプトを使用して削除するにはどうすればいいですか?

まず、この Redis 固有のスクリプトを見てみましょう。

  1. redis.call( "get" 、KEYS[1]) == ARGV[1]の場合
  2. それから 
  3. redis.call( "del" ,KEYS[1])を返す
  4. それ以外 
  5. 0を返す
  6. 終わり 

このスクリプトは、プラチナ プランのキーの取得および削除の方法と非常によく似ています。まず、KEYS[1]の値を取得し、KEYS[1]の値がARGV[1]の値と等しいかどうかを判断します。等しい場合はKEYS[1]を削除します。

では、このスクリプトを Java プロジェクトでどのように実行するのでしょうか?

2 つのステップがあります。まずスクリプトを定義します。次に、redisTemplate.execute メソッドを使用してスクリプトを実行します。

  1. // スクリプトのロックを解除する
  2. 文字列スクリプト = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" ;
  3. redisTemplate.execute (新しい DefaultRedisScript<Long>(script, Long.class), Arrays.asList( "lock" ), uuid);

上記のコードでは、KEYS[1]は「lock」に対応し、ARGV[1]は「uuid」に対応しています。つまり、lockの値がuuidと等しい場合、lockは削除されます。

この Redis スクリプトは、Redis に組み込まれた Lua 環境によって実行されるため、Lua スクリプトとも呼ばれます。

ダイヤモンドソリューションは完璧ですか?もっと良い解決策はあるでしょうか?

次の記事では、分散ロックのもう 1 つの優れたソリューションである Redisson を紹介します。

8. 結論

この記事では、分散ロックの問題をローカル ロックの問題を通じて拡張します。次に、5 つの分散ロック ソリューションが紹介され、最も単純なものから最も複雑なものまで、さまざまなソリューションの改善点が説明されました。

上記のソリューションの継続的な進化により、システム内のどこに異常な状況が存在する可能性があるか、また、それをより適切に処理する方法がわかります。

同様に、この進化する考え方は他のテクノロジーにも適用できます。

以下に、上記 5 つのソリューションの欠点と改善点をまとめます。

ブロンズプラン:

  • 欠陥: ビジネス コードで例外が発生したり、サーバーがクラッシュしたりすると、ロックを積極的に削除するロジックが実行されず、デッドロックが発生します。
  • 改善: ロックの自動有効期限を設定します。一定時間が経過すると、ロックは自動的に削除され、他のスレッドがロックを取得できるようになります。

シルバープラン:

  • 欠陥: ロックの占有とロックの有効期限の設定は、アトミック操作ではなく、2 つのステップで実行されます。
  • 改善: ロック占有とロック有効期限の設定により、アトミック操作が保証されます。

ゴールドソリューション:

  • 不具合: ロックをアクティブに削除する場合、ロック値が同じであるため、他のクライアントが占有しているロックが削除されます。
  • 改善: ロックが占有されるたびに、ランダムに大きな値が設定されます。ロックをアクティブに削除する場合は、ロック値と設定した値を比較して、等しいかどうかを確認します。

プラチナプラン:

  • 欠陥: ロックの取得、ロック値の比較、およびロックの削除は非アトミックなステップです。ロックはプロセス中に自動的に期限切れになり、他のクライアントによって取得され、その結果、他のクライアントによって占有されているロックが削除される可能性があります。
  • 改善: Lua スクリプトを使用して、ロックの取得、ロックの比較、ロックの削除などのアトミック操作を実行します。

ダイヤモンドプラン:

  • 欠点: プロフェッショナルではない分散ロック ソリューション。
  • 改善: 分散ロックを再構築します。

王様の計画、また次回の記事でお会いしましょう〜

上記のコードはすべて、PassJava オープン ソース プロジェクトに基づいています。バックエンド、フロントエンド、ミニプログラムは同じリポジトリにアップロードされ、GitHub または Code Cloud からアクセスできます。住所は以下の通りです。

Github: https://github.com/Jackson0714/PassJava-Platform

コードクラウド: https://gitee.com/jayh2018/PassJava-Platform

サポートチュートリアル: www.passjava.cn

参考文献:

http://redis.cn/commands/set.html

https://www.bilibili.com/video/BV1np4y1C7Yf

この記事はWeChatの公開アカウント「Wukong Chats about Architecture」から転載したものです。下のQRコードからフォローできます。この記事を転載する場合はWukong Chat Architecture公式アカウントまでご連絡ください。

<<:  「仮想IP」に関する漫画

>>:  Kafka 2.8.0 がリリースされ、正式に ZooKeeper から分離しました。

推薦する

オンラインデートは米国で人気があり、数十億ドル規模の産業を生み出している

インターネットは、人々が愛を見つけるための重要な場所になりつつある中国国際放送、北京、2月11日のニ...

効率性の束縛を打ち破るUAI-Trainにより、ARKieは設計ニーズをより深く理解できるようになります

過去2年間で、人工知能(AI)は研究・概念レベルから応用レベルへと徐々に移行し、ますます多くの企業が...

画像とテキストのストーリー: JVM の世界へ誘う記事

[[428766]]最近、私はほとんどの余暇時間を、シンプルな RPC フレームワーク (初心者でも...

クラウド市場での競争が激化する中、通信事業者はどうすれば差別化できるのでしょうか?

政策の成果と市場の需要に後押しされ、我が国のクラウド コンピューティング市場の規模は拡大を続け、産業...

高品質のブログを育成して高品質の外部リンクを作成する

ブログは、ウェブログ、ブログ、またはブログとも呼ばれ、個人によって管理され、新しい記事が随時公開され...

Hacking Teamのリモートコントロールシステムの簡単な分析

7月5日夜、イタリアの遠隔操作ソフトメーカー「ハッキングチーム」の内部情報が流出した。その衝撃はスノ...

世界初! Crane は、コストを削減し効率を高める FinOps 向けの最初の認定オープンソース ソリューションになります。

ちょうど今、Tencent Cloud のオープンソース プロジェクト Crane (クラウド リソ...

APPプロモーション:6大Androidアプリマーケットにおける基本的なASO最適化を徹底解説!

始めたばかりの頃は、オンラインでさまざまな記事やチュートリアルを読み、さまざまな分野を探索し、絶えず...

クラウドコンピューティングの「特異点」は2021年に第2の爆発をもたらし、5つの新しいトレンドが出現する

2020年、世界はユニークな形で新たな章を開きました。新型コロナウィルスの流行の影響により、全世界が...

携帯電話の通信を遮断しないという政策が正式に施行される

9月29日午後、大手3社は本日同時に通知を出し、携帯電話パッケージの月間通信量を未確定にするサービス...

アリペイ、P2P資金移動協力を一時停止:新たな契約は締結されない

「アリペイのコンプライアンス部門は最近、すべてのP2P資金フロー協力にいくつかの調整を加えることを決...

ウェブサイトのキーワードランキングが下がった理由と解決策をまとめます

このタイトルを見ると、誰かが以前に理由をまとめたことに誰もが間違いなく気づいたでしょうが、Xiao ...

通信事業者がクラウドとエッジコンピューティングを統合

調査会社ガートナーは、企業が生成するデータのうち、従来の集中型データセンターやクラウド プラットフォ...