Redis分散ロックの8つの落とし穴について話す

Redis分散ロックの8つの落とし穴について話す

[[425706]]

序文

分散システムでは、Redis 分散ロックは比較的シンプルで効率的であり、多くの実際のビジネス シナリオで使用されているため、分散ロックの第一選択肢となっています。

しかし、Redis 分散ロックを使用したからといって、安心してリラックスできるわけではありません。適切に使用しなかったり、正しく使用しなかったりすると、予期しない問題が発生する可能性もあります。

今日は、Redis 分散ロックの落とし穴についていくつか説明し、困っている友人に参考情報を提供します。

1 非アトミック操作

Redis 分散ロックを使用する場合、最初に思い浮かぶのは setNx コマンドかもしれません。

  1. jedis.setnx(lockKey, val) == 1 の場合 {
  2. jedis.expire(lockKey、タイムアウト);
  3. }

簡単です。すぐにコードを書くことができます。

このコードは確かに正常にロックできますが、何か問題は見つかりましたか?

ロック操作とその後のタイムアウトの設定は別々であり、アトミック操作ではありません。

ロックは成功したが、タイムアウト設定に失敗した場合、lockKey は永続的になります。同時実行性の高いシナリオで、多数の lockKey が正常にロックされても無効にならない場合、Redis のメモリ領域が不足する可能性があります。

では、アトミック性を保証するロック コマンドはあるのでしょうか?

答えは「はい」です。下記をご覧ください。

2 ロックを解除し忘れた

前述のように、setNx コマンドを使用したロック操作とタイムアウト設定は別々であり、アトミック操作ではありません。

また、redis には複数のパラメータを指定できる set コマンドもあります。

  1. 文字列結果 = jedis.set ( lockKey, requestId, "NX" , "PX" , expireTime);
  2. if ( "OK" .equals(結果)) {
  3. 戻る 真実;
  4. }
  5. 戻る 間違い;

で:

  • lockKey: ロック識別子
  • requestId: リクエストID
  • NX: キーが存在しない場合にのみ設定します。
  • PX: キーの有効期限を millisecond ミリ秒に設定します。
  • 有効期限: 有効期限

set コマンドはアトミック操作です。ロックとタイムアウトの設定は、1 つのコマンドで簡単に実行できます。

ニース

set コマンドを使用してロックすると、表面的には問題はないように見えます。しかし、よく考えてみると、ロックした後、毎回タイムアウト期間が経過した後にのみロックを解除するのは不合理です。ロックした後、ロックが時間内に解除されない場合、多くの問題が発生します。

分散ロックのより合理的な使用法は次のとおりです。

  1. 手動ロック
  2. 事業運営
  3. 手動リリースロック
  4. ロックの手動解除に失敗した場合、タイムアウト期間に達すると、Redis は自動的にロックを解除します。

一般的なフローチャートは次のとおりです。

それで、問題は、どうやってロックを解除するかということです。

疑似コードは次のとおりです。

  1. 試す{
  2. 文字列結果 = jedis.set (lockKey、requestId、 "NX" "PX" 、expireTime);
  3. if ( "OK" .equals(結果)) {
  4. 戻る 真実;
  5. }
  6. 戻る 間違い;
  7. ついに
  8. ロックを解除します(lockKey);
  9. }

ビジネス コードで例外をキャッチし、最後にロックを解除する必要があります。つまり、コードが正常に実行されるか失敗するかに関係なく、ロックを解除する必要があります。

この時点で、一部の友人は次のように尋ねるかもしれません。ロックを解除するときに、システムが再起動されたり、ネットワークが切断されたり、コンピューター ルームがダウンしたりした場合、ロックの解除に失敗するのではないでしょうか。

この小さな確率の問題は実際に存在するので、これは良い質問です。

しかし、先ほどロックにタイムアウトを設定したことを覚えていますか?異常な状況によりロックの解除に失敗しても、設定したタイムアウトに達すると、Redis によってロックが自動的に解除されます。

しかし、最終的にロックを解除するだけで十分でしょうか?

3 他の人のロックを解除した

親切な人になるために、まず上記の質問に答えましょう。ロックを解除する姿勢がまだ間違っているため、最後にロックを解除するだけでは確かに十分ではありません。

どうしたの?

回答: マルチスレッドのシナリオでは、他のユーザーのロックが解除される可能性があります。

何人かの友人は反論するかもしれません: マルチスレッドのシナリオで、スレッド A がロックを取得したとします。しかし、スレッド A がロックを解除しないと、スレッド B はロックを取得できないので、他のスレッドのロックが解除されたと言えるのでしょうか?

回答: スレッド A とスレッド B の両方が lockKey を使用してロックする場合。スレッド A は正常にロックされましたが、ビジネス機能の実行に時間がかかり、設定されたタイムアウト期間を超えています。このとき、redis は lockKey ロックを自動的に解除します。この時点で、スレッド B は lockKey を正常にロックし、ビジネス操作を実行できます。この時点で、スレッド A はビジネス機能を完了し、finally メソッドでロック lockKey を解放します。これは問題です。スレッド B のロックはスレッド A によって解除されます。

おそらくこのとき、スレッドBはトイレで泣きながら気を失い、まだ意味不明なことを言い続けていたと思います。

では、この問題をどう解決すればいいのでしょうか?

set コマンドを使用してロックする場合、lockKey ロック識別子の使用に加えて、requestId という追加パラメータが設定されていることに気付きましたか。 requestId を記録する必要があるのはなぜですか?

回答: requestId はロックを解除するときに使用されます。

疑似コードは次のとおりです。

  1. jedis.get(lockKey).equals(requestId) の場合 {
  2. jedis.del(ロックキー);
  3. 戻る 真実;
  4. }
  5. 戻る 間違い;

ロックを解除するときは、まずロック値(前回設定した値は requestId )を取得し、前回設定した値と同じかどうかを判断します。同じ場合は、ロックの削除が許可され、成功が返されます。異なる場合は、失敗が直接返されます。

つまり、自分が追加したロックのみを解除でき、他のユーザーが追加したロックを解除することはできません。

ここで requestId を使用する必要があるのはなぜですか? userIdは使えませんか?

回答: userId が使用されている場合、リクエストに対して一意ではありません。複数の異なるリクエストで同じユーザー ID が使用される場合があります。 requestId はグローバルに一意であるため、ロックのロックと解除で混乱が生じることはありません。

さらに、Lua スクリプトを使用すると、他の人のロックを解除する問題も解決できます。

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

lua スクリプトを使用すると、ロックが存在するかどうかを照会したり、ロックを削除したりすることがアトミック操作であることを保証できるため、これを使用してロックを解除するとより効果的です。

Lua スクリプトについて言えば、実際にはロック操作には Lua スクリプトを使用することをお勧めします。

  1. (redis.call( 'exists' , KEYS[1]) == 0)の場合 
  2. redis.call( 'hset' , キー[1], ARGV[2], 1);
  3. redis.call( 'pexpire' , KEYS[1], ARGV[1]);
  4. nilを返します
  5. 終わり 
  6. (redis.call( 'hexists' , KEYS[1], ARGV[2]) == 1)の場合
  7. redis.call( 'hincrby' , キー[1], ARGV[2], 1);
  8. redis.call( 'pexpire' , KEYS[1], ARGV[1]);
  9. nilを返します
  10. 終わり;
  11. redis.call( 'pttl' , KEYS[1] )を返します

これは、redisson フレームワークのロック コードです。よく書かれているので参考にしてください。

面白いですね。他に何か楽しいことはありますか?

4. 失敗したリクエストの数が多い

上記のロック方法は問題ないように思えますが、よく考えてみると、ロックを巡って同時に 10,000 件のリクエストが競合した場合、1 件のリクエストのみが成功し、残りの 9,999 件のリクエストは失敗する可能性があります。

フラッシュセールのシナリオではどのような問題が生じますか?

A: 10,000 件のリクエストごとに 1 件が成功します。 10,000 件のリクエストのうち 1 件が成功しました。在庫がなくなるまでこれが続きます。これは、私たちが想像していたものとは違って、均等に配布されたフラッシュセールになります。

この問題を解決するにはどうすればいいでしょうか?

さらに、別のシナリオもあります。

たとえば、2 つのスレッドが同時に sftp にファイルをアップロードする場合、ファイルをアップロードする前にディレクトリを作成する必要があります。 2 つのスレッドが作成する必要があるディレクトリの名前が両方とも日付 (例: 20210920) であると仮定します。制御が実行されず、ディレクトリが同時に作成されると、2 番目のスレッドは必然的に失敗します。

このとき、一部の友人はこう言うかもしれません: 「これは簡単ではない。Redis 分散ロックを追加すれば問題は解決できる。」さらに、別の判断を下します。ディレクトリがすでに存在する場合は作成されません。ディレクトリが存在しない場合にのみ作成する必要があります。

疑似コードは次のとおりです。

  1. 試す {
  2. 文字列結果 = jedis.set (lockKey、requestId、 "NX" "PX" 、expireTime);
  3. if ( "OK" .equals(結果)) {
  4. if (!exists (パス)) {
  5. mkdir(パス);
  6. }
  7. 戻る 真実;
  8. }
  9. } ついに{
  10. ロックを解除します(ロックキー、リクエストID);
  11. }
  12. 戻る 間違い;

すべては良さそうに見えますが、慎重な精査には耐えられません。

魂からの質問: 2 番目のリクエストがロックに失敗した場合、失敗が返されるのでしょうか、それとも成功が返されるのでしょうか?

主なフローチャートは次のとおりです。

明らかに、2 番目のリクエストは失敗を返すことはできません。失敗が返された場合、問題はまだ解決されていません。ファイルが正常にアップロードされていない場合、直接 success を返すと、より大きな問題が発生します。頭痛を解決するには?

A: スピンロックを使用します。

  1. 試す {
  2. ロングスタート = System.currentTimeMillis();
  3. while( true ) {
  4. 文字列結果 = jedis.set (lockKey、requestId、 "NX" "PX" 、expireTime);
  5. if ( "OK" .equals(結果)) {
  6. if (!exists (パス)) {
  7. mkdir(パス);
  8. }
  9. 戻る 真実;
  10. }
  11.       
  12. 長い時間= System.currentTimeMillis() - 開始;
  13. if (時間>= タイムアウト ) {
  14. 戻る 間違い;
  15. }
  16. 試す {
  17. スレッド.sleep(50);
  18. } キャッチ (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. } ついに{
  23. ロックを解除します(ロックキー、リクエストID);
  24. }
  25. 戻る 間違い;

指定された時間内(500 ミリ秒など)に、スピンは継続的にロックを試行し(簡単に言えば、無限ループで継続的にロックを試行します)、成功した場合は直接戻ります。失敗した場合は、50 ミリ秒間スリープしてから、新たな試行ラウンドを開始します。タイムアウト期間が経過してもロックが取得されない場合は、直接失敗が返されます。

はい、1つのトリックを学びました。他に何かありますか?

5 ロックの再入問題

Redis の分散ロックは相互に排他的であることは誰もが知っています。キーをロックし、そのキーに対応するロックの有効期限が切れていない場合、同じキーを使用して再度ロックすると失敗する可能性が高くなります。

はい、ほとんどのシーンは大丈夫です。

なぜほとんどのシーンですか?

こんなシーンもあるので:

あるリクエストで、条件を満たすメニューツリーまたはカテゴリツリーを取得する必要があるとします。メニューを例に挙げてみましょう。これには、インターフェイスのルート ノードから開始し、条件を満たすすべての子ノードを再帰的に走査し、それらをメニュー ツリーに組み立てる必要があります。

メニューは静的ではないことに注意してください。バックエンド システムの運用スタッフは、メニューを動的に追加、変更、削除できます。同時実行時に常に最新のデータを取得できるようにするには、ここで Redis 分散ロックを追加できます。

Redis 分散ロックを追加するという考えは正しいです。しかし、ここで問題が発生します。再帰方式では、再帰的なトラバーサルが複数回繰り返され、そのたびに同じロックが追加されます。もちろん、最初のレベルの再帰は正常にロックできますが、2 番目、3 番目、および N レベルの再帰が完了するとロックは失敗しませんか?

再帰メソッドでロックするための疑似コードは次のとおりです。

  1. プライベートint有効期限 = 1000;
  2.  
  3. パブリックvoid fun( int  レベル、文字列ロックキー、文字列リクエストID){
  4. 試す{
  5. 文字列結果 = jedis.set (lockKey、requestId、 "NX" "PX" 、expireTime);
  6. if ( "OK" .equals(結果)) {
  7. if(レベル<=10){
  8. this.fun(++レベル、ロックキー、リクエスト ID);
  9. }それ以外{
  10. 戻る;
  11. }
  12. }
  13. 戻る;
  14. ついに
  15. ロックを解除します(ロックキー、リクエストID);
  16. }
  17. }

そのまま使用しても問題ないようです。しかし、プログラムを実行すると、例外が発生するという結果しか発生しないことがわかります。

ルートノードから開始して、最初のレベルの再帰ロックが成功し、ロックが解除される前に、直接 2 番目のレベルの再帰に入るためです。ロック名が lockKey であり、値が requestId のロックがすでに存在するため、第 2 レベルの再帰はロックに失敗し、第 1 レベルに戻る可能性が高くなります。その後、最初のレイヤーはロックを通常どおり解放し、再帰メソッド全体が直接戻ります。

さて、皆さんは何が問題なのか分かっているでしょう?

そうです、再帰メソッドは実際には最初のレベルの再帰のみを実行して戻ります。ロックの失敗により、他のレベルの再帰はまったく実行できません。

では、この問題をどう解決すればよいのでしょうか?

A: 再入可能ロックを使用します。

内部的に再入可能ロック機能を実装している redisson フレームワークを例に挙げてみましょう。

昔から「陳金南を知らなければ英雄を名乗っても無駄だ」という諺があります。

私はこう言いました: 分散ロックが再分配を認識しない場合、それを良いロックと呼ぶのは無意味です。ハハハ、ただ楽しんでるだけだよ。

redisson は redis 分散ロックにおいて高い地位を占めていることがわかります。

疑似コードは次のとおりです。

  1. プライベートint有効期限 = 1000;
  2.  
  3. パブリックvoid run(String lockKey) {
  4. RLock ロック = redisson.getLock(lockKey);
  5. this.fun(ロック,1);
  6. }
  7.  
  8. パブリックvoid fun(RLock ロック, int  レベル){
  9. 試す{
  10. ロック。ロック(5, 時間単位.SECONDS);
  11. if(レベル<=10){
  12. this.fun(ロック、++レベル);
  13. }それ以外{
  14. 戻る;
  15. }
  16. ついに
  17. ロックを解除します。
  18. }
  19. }

上記のコードは完璧ではないかもしれませんが、一般的なアイデアを示しているだけです。このような必要がある場合、上記のコードは参考用です。

次に、redisson 再入可能ロックの実装原理について説明します。

ロックは主に次のスクリプトによって実現されます。

  1. redis.call( 'exists' , KEYS[1]) == 0の場合
  2. それから   
  3. redis.call( 'hset' , キー[1], ARGV[2], 1); redis.call( 'pexpire' , KEYS[1], ARGV[1]);
  4. nilを返します
  5. 終わり;
  6. (redis.call( 'hexists' , KEYS[1], ARGV[2]) == 1)の場合
  7. それから   
  8. redis.call( 'hincrby' , キー[1], ARGV[2], 1);
  9. redis.call( 'pexpire' , KEYS[1], ARGV[1]);
  10. nilを返します
  11. 終わり;
  12. redis.call( 'pttl' , KEYS[1] )を返します

で:

  • KEYS[1]: ロック名
  • ARGV[1]: 有効期限
  • ARGV[2]: uuid + ":" + threadId(requestIdとみなすことができる)
  1. まずロック名が存在しないかどうかを確認してからロックします。
  2. 次に、ロック名と requestId 値の両方が存在する場合は、hincrby コマンドを使用して、ロック名と requestId 値をカウントし、そのたびに 1 を加算します。これは再入可能ロックの鍵となることに注意してください。ロックが再入力されるたびに、値は 1 ずつ増加します。
  3. ロック名が存在するが値が requestId でない場合は、有効期限が返されます。

ロックの解除は主に次のスクリプトによって行われます。

  1. (redis.call( 'hexists' , KEYS[1], ARGV[3]) == 0の場合)
  2. それから  
  3. ゼロを返す
  4. 終わり 
  5. ローカルカウンター = redis.call( 'hincrby' , KEYS[1], ARGV[3], -1);
  6. (カウンタ>0)の場合
  7. それから  
  8. redis.call( 'pexpire' , KEYS[1], ARGV[2]);
  9. 0を返します
  10. それ以外  
  11. redis.call( 'del' , キー[1]);
  12. redis.call( 'publish' , KEYS[2], ARGV[1]);
  13. 1 を返します
  14. 終わり;
  15. ゼロを返す
  1. まず、ロック名と requestId 値が存在しないかどうかを確認し、直接戻ります。
  2. ロック名と requestId 値が存在する場合、再入可能ロックは 1 減少します。
  3. 再入ロックの値が 1 減算された後も 0 より大きい場合は、まだ参照が存在していることを意味するため、有効期限を再度設定してみてください。
  4. 再入ロックの値が 1 減算された後も 0 のままである場合、ロックは削除され、待機中のスレッドにロックを取得するように通知するメッセージが送信されます。

システムが一時的なデータの不整合を許容できる場合は、いくつかのシナリオでロックしなくても問題ないことを再度強調しておきます。ここでは単なる例を示しており、このセクションの内容はすべてのシナリオに当てはまるわけではありません。

6 ロック競合の問題

データの書き込みが必要なビジネス シナリオが多数ある場合は、通常の Redis 分散ロックを使用しても問題ありません。

ただし、ビジネス シナリオによっては、書き込み操作は比較的少ないものの、読み取り操作は多数発生する場合があります。通常の Redis 分散ロックを直接使用するとパフォーマンスが無駄になりますか?

ロックの粒度が粗いほど、ロックをめぐる複数のスレッド間の競争が激しくなり、複数のスレッドがロックするまでの待機時間が長くなり、パフォーマンスが低下することは誰もが知っています。

したがって、Redis 分散ロックのパフォーマンスを向上させるための最初のステップは、ロックの粒度をより細かくすることです。

6.1 読み取り/書き込みロック

ご存知のとおり、ロックの目的は、同時実行環境でのデータの読み取りと書き込みのセキュリティを確保すること、つまり、データ エラーや不整合が発生しないようにすることです。

ただし、実際のビジネス シナリオのほとんどでは、データの読み取り頻度はデータの書き込み頻度よりもはるかに高くなります。スレッド間の同時読み取り操作では、同時安全性の問題は発生しません。読み取り操作にミューテックス ロックを追加する必要はありません。読み取り/書き込みと書き込み/書き込みの同時操作のロックが相互に排他的であることを確認するだけで、システムのパフォーマンスが向上します。

内部的に読み取り/書き込みロック機能を実装している redisson フレームワークを例に挙げてみましょう。

読み取りロックの疑似コードは次のとおりです。

  1. RReadWriteLock readWriteLock = redisson.getReadWriteLock( "readWriteLock" );
  2. RLock rLock = readWriteLock.readLock();
  3. 試す {
  4. rLock.ロック();
  5. //事業運営
  6. } キャッチ (例外 e) {
  7. ログエラー(e);
  8. ついに
  9. rLock.ロック解除();
  10. }

ロックを書き込むための疑似コードは次のとおりです。

  1. RReadWriteLock readWriteLock = redisson.getReadWriteLock( "readWriteLock" );
  2. RLock rLock = readWriteLock.writeLock();
  3. 試す {
  4. rLock.ロック();
  5. //事業運営
  6. } キャッチ (InterruptedException e) {
  7. ログエラー(e);
  8. ついに
  9. rLock.ロック解除();
  10. }

読み取りロックと書き込みロックを分離する最大の利点は、読み取りと読み取りが共有され、相互排他性がなくなるため、読み取り操作のパフォーマンスが向上することです。実際のビジネス シナリオでは、ほとんどのデータ操作は読み取り操作です。したがって、読み取り操作のパフォーマンスが向上すると、ロック全体のパフォーマンスも向上します。

読み取り/書き込みロックの特徴を以下にまとめます。

  • 読書と読書は排他的ではなく共有される
  • 読み取りと書き込みは相互に排他的です
  • 書き込みと書き込みは相互に排他的です

6.2 ロックのセグメンテーション

さらに、ロックの粒度を下げるために、大きなロックをセグメントに分割するのが一般的です。

Java では、ConcurrentHashMap はデータを 16 個のセグメントに分割し、各セグメントには個別のロックがあり、異なるロック セグメントのデータは互いに干渉しないため、ロックのパフォーマンスが向上します。

実際のビジネス シナリオでは、次のことが可能です。

たとえば、在庫を差し引くことができるフラッシュ セールのシナリオでは、在庫が 2,000 個あり、ユーザーはフラッシュ セールでそれらを購入できます。過剰販売を防ぐために、通常は在庫をロックすることができます。同じロックをめぐって 10,000 人のユーザーが競合する場合、システムのスループットは明らかに非常に低くなります。

システムのパフォーマンスを向上させるために、在庫をセグメントに分割することができます。たとえば、100 セグメントに分割すると、各セグメントにフラッシュ セールに参加できる製品が 20 個ずつあります。

フラッシュセールのプロセス中に、まずユーザー ID のハッシュ値を取得し、それを 100 で割って係数を取得します。モジュラス 1 のユーザーは在庫の最初のセクションにアクセスし、モジュラス 2 のユーザーは在庫の 2 番目のセクションにアクセスし、モジュラス 3 のユーザーは在庫の 3 番目のセクションにアクセスし、モジュラス 100 のユーザーは在庫の 100 番目のセクションにアクセスするまで、同様に続きます。

この方法により、マルチスレッド環境ではロックの競合を大幅に削減できます。以前は、複数のスレッドが同時に競合できるのは 1 つのロックのみでした。特にフラッシュセールのシナリオでは、競争があまりにも激しく、非人道的と言えるほどでした。その結果、スレッドの大部分がロックを待機していました。複数のスレッドが同時に 100 個のロックを求めて競合するため、待機中のスレッドが少なくなり、システムのスループットが向上します。

ロックをセグメント化するとシステムのパフォーマンスは向上しますが、システムの複雑さも増すことに注意してください。追加のルーティング アルゴリズム、セグメント間統計、およびその他の機能の導入が必要になるためです。実際のビジネス シナリオでは、包括的に検討する必要がありますが、これはロックをセグメント化する必要があるという意味ではありません。

7 ロックタイムアウトの問題

先ほど、スレッド A が正常にロックしたが、ビジネス関数に時間がかかり、設定されたタイムアウトを超えた場合、Redis はスレッド A によって追加されたロックを自動的に解放すると述べました。

タイムアウト期間が来るとロックが解除され、機能には影響がなくなる、と言う友人もいるかもしれません。

答え: 間違っています、間違っています、間違っています。実際に機能に影響します。

通常、ロックの目的は、重要なリソースにアクセスするときにデータの異常を防ぐことです。たとえば、スレッド A がデータ C の値を変更し、スレッド B もデータ C の値を変更しているとします。制御が行われない場合、同時実行状況でデータ C の値が問題になります。

特定のメソッドまたはコード セグメントの相互排他性を保証するために、つまり、スレッド A が特定のコード セグメントを実行する場合、他のスレッドが同時にそれを実行できないようにするために、 synchronized キーワードを使用してロックすることができます。

ただし、このタイプのロックには大きな制限があり、単一のノードの相互排他性しか保証できません。複数のノード間で排他性を維持する必要がある場合は、Redis 分散ロックを使用する必要があります。

ここまで準備を整えたので、本題に戻りましょう。

スレッド A のコードと Redis 分散ロックに、コード 1 とコード 2 という 2 つのコード セグメントが含まれているとします。

このスレッドで実行される業務処理には非常に時間がかかるため、プログラムがコード 1 の実行を終了すると、設定されたタイムアウト期間が経過し、Redis は自動的にロックを解除します。コード 2 はまだ実行されていません。

この時点では、コード 2 はネイキッド状態と同等であり、相互排他性は保証されません。重要なリソースにアクセスし、他のスレッドもそのリソースにアクセスすると、データ異常が発生する可能性があります。 (追記:重要なリソースへのアクセスとは、読むことだけでなく書くことも意味します)

では、この問題をどう解決すればいいのでしょうか?

回答: タイムアウト期間に達したがビジネス コードが実行されていない場合は、ロックを自動的に更新する必要があります。

自動更新機能を実装するには、TimerTask クラスを使用します。

  1. タイマー timer = new Timer();
  2. timer.schedule(new TimerTask() {
  3. @オーバーライド
  4. パブリックvoid run(Timeout timeout) 例外をスローします {
  5. //自動更新ロジック
  6. }
  7. }, 10000, TimeUnit.MILLISECONDS);
  8.          

ロックを取得すると、スケジュールされたタスクが自動的に開始され、有効期限が 10 秒ごとに自動的に更新されます。このメカニズムは、Redisson フレームワークではより威圧的な名前を持っています。それは、伝説的なウォッチドッグである watch dog です。

もちろん、自動更新機能については、次のような lua スクリプトを使用することをお勧めします。

  1. (redis.call( 'hexists' , KEYS[1], ARGV[2]) == 1)場合  
  2. redis.call( 'pexpire' , KEYS[1], ARGV[1]);
  3. 1 を返します
  4. 終わり;
  5. 0を返します

自動更新機能を実装する場合は、合計有効期限を設定する必要があることに注意してください。これは、redisson と一致し、30 秒に設定できます。ビジネス コードがこの合計有効期限に達しても実行されていない場合は、自動的に更新されなくなります。

自動更新機能は、ロックを取得した後にスケジュールされたタスクを開始し、10 秒ごとにロックが存在するかどうかを確認します。その場合、有効期限が更新されます。 3回更新後30秒経過してもサービスメソッドが実行されない場合は更新されません。

8 マスタースレーブレプリケーションの問題

上記で紹介した内容は、単一の Redis インスタンスでは問題になりません。

しかし、Redis のインスタンスが複数ある場合。たとえば、マスタースレーブシステムを使用したり、センチネルモードを使用したりすると、Redis ベースの分散ロック機能に問題が発生します。

いったい何が問題なのでしょうか?

Redis は現在、マスター ノード 1 つとスレーブ ノード 3 つでマスター スレーブ モードを使用していると仮定します。マスターノードはデータの書き込みを担当し、スレーブノードはデータの読み取りを担当します。

彼らはもともと調和と平和の中で暮らしていました。 Redis のロック操作はすべてマスター上で実行されます。ロックが成功すると、すべてのスレーブに非同期的に同期されます。

ある日突然、何らかの不可逆的な理由によりマスターノードがクラッシュしました。

この場合、slave1 が選出されたと仮定して、新しいマスター ノードにアップグレードするスレーブを見つける必要があります。

ロックAがあったらかなり残念です。ロックが正常に追加された直後にマスターがクラッシュし、スレーブ 1 に同期する時間がありません。

これにより、新しいマスター ノードのロック A が失われます。その後、新しいスレッドがロック A を使用してロックすると、そのスレッドは引き続き成功し、分散ロックは無効になります。

では、この問題をどう解決すればいいのでしょうか?

回答: この問題を解決するために、redisson フレームワークは、Redlock アルゴリズムを使用する RedissonRedLock という特別なクラスを提供します。

RedissonRedLock は次のように問題を解決します。

  1. いくつかの独立した Redis 環境を構築する必要があります。ここで 5 つ構築するとします。
  2. 各環境には redisson ノードがあります。
  3. 複数の redisson ノードが RedissonRedLock を構成します。
  4. 環境には、スタンドアロン、マスタースレーブ、センチネル、クラスター モードが含まれ、これらを 1 つ以上組み合わせることもできます。

ここではマスタースレーブを例にとり、アーキテクチャ図は次のようになります。

RedissonRedLock のロック プロセスは次のとおりです。

  1. すべての redisson ノード情報を取得し、ループ内のすべての redisson ノードをロックします。ノード数が N であると仮定します。この例では 5 です。
  2. N 個のノードのうち N/2 + 1 個のノードが正常にロックされた場合、RedissonRedLock ロック全体が成功します。
  3. N 個のノードのうち N/2 + 1 個未満のノードが正常にロックされた場合、RedissonRedLock ロック全体が失敗します。
  4. 各ノードをロックするのにかかる合計時間が設定された最大待機時間以上であることが判明した場合、直接失敗が返されます。

上記から、マスターノードがハングアップした場合、Redlock アルゴリズムを使用すると、マルチインスタンス シナリオでの分散ロック障害の問題を実際に解決できることがわかります。

しかし、次のような新たな疑問も生じます。

複数の追加環境を構築し、より多くのリソースを申請する必要があり、コストと費用対効果を評価する必要があります。

N 個の再同期ノードがある場合、再同期が正常にロックされたかどうかを確認するには、それらを N 回、または少なくとも N/2+1 回ロックする必要があります。明らかに、余分な時間コストは価値がありません。

実際のビジネス シナリオ、特に同時実行性の高いビジネスでは、RedissonRedLock はあまり使用されていないことがわかります。

分散環境では、CAP を避けることはできません。

CAP は分散システムを指します。

  • 一貫性
  • 可用性
  • パーティション耐性

これら 3 つの要素のうち最大 2 つを同時に達成できます。これら3つすべてを達成することは不可能です。

実際のビジネス シナリオでより高いデータの一貫性が必要な場合。次に、Zookeeper などの CP タイプの分散ロックを使用してください。これはディスクベースであり、最高のパフォーマンスが得られない可能性がありますが、通常、データは失われません。

実際のビジネス シナリオでは、さらに高いデータ可用性を確保することが必要になります。その場合は、メモリベースでパフォーマンスが優れているが、データ損失のリスクがある、Redis などの AP タイプの分散ロックを使用してください。

実際、ほとんどの分散ビジネス シナリオでは、Redis 分散ロックを使用するだけで十分です。あまり真剣に考えすぎないでください。データの不整合の問題は、最終的な一貫性ソリューションを通じて解決できます。しかし、システムが利用できない場合は、ユーザーに 10,000 ポイントのダメージを与えるクリティカル ヒットが発生します。

この記事はWeChatのパブリックアカウント「Su San Talks Technology」から転載したものです。

<<:  エッジコンピューティングの5つの典型的なアプリケーションシナリオ

>>:  ローコード開発は混乱状態にあります。企業はどのように正しい選択をすべきでしょうか?

推薦する

とんでもない!なぜ面接ではいつもカフカについて質問されるのでしょうか?急いで埋め合わせをしましょう

みなさんこんにちは。私はNezhaです。 Kafka は、今日の時代におけるデータ パイプラインのほ...

Baidu はどのような種類の Web サイトをインデックスする傾向がありますか?

私たちの周りには、「たくさんのお金をかけてとても美しいウェブサイトを作ったのに、なぜ百度で私たちのウ...

マイクロビジネスは死んだ

路上で誰かを迎えに行くと、彼らはわずか3文でB2BO2OP2Pについて話しますこれは、もう少し関心を...

gigsgigscloud: スピードと高防御の両方を考慮した香港の高防御VDS、CN2高速ネットワーク+1Tbps DDoS防御+Jingdun CC防御、

gigsgigscloud には、速度、超高 DDoS 防御、CC 攻撃防御を特徴とする新しい香港 ...

週刊ニュースレビュー:CCTVが価格比較ソフトウェアWochachaがQvodに対する恐喝に関与し、巨額の罰金を科せられたことを明らかに

1. CCTVが価格比較ソフトウェアWochachaが恐喝に関与していたことを暴露:お金を払えば価格...

12年間の苦闘を経て、アリババクラウドはついに利益を上げることに成功した。クラウドコンピューティングは本当に良いビジネスなのでしょうか?

クラウドコンピューティングは注目の分野であり、さまざまな大手企業が参入を急いでいます。中国では、Al...

修正済み - 30% 割引コード/40G 高防御 VPS/KVM/無制限トラフィック/ロサンゼルス

Rectified の 11 月の大きなプロモーションが始まりました: Sharktech のロサン...

ユーザーエクスペリエンスはSEOの永遠の命題である

昨日の午後、Baidu アルゴリズムが再び更新されたというニュースを、SEO 業界の多くの友人が受け...

停電により、コンピュータルームの多くのB2Cウェブサイトが麻痺した。

昨日はヴァンクルの5周年記念セールの日だったが、朝からウェブサイトが麻痺し、ウェブページを開くことが...

Webmaster Network ニュース: Microsoft XP システムが本日「引退」、JD.com のラブストーリーが再び熱を帯びる

1. Microsoft XP システムが本日「廃止」され、数億人の中国ユーザーがセキュリティリスク...

コンテナ クラウドで分散ストレージを使用する場合、HDFS、CEPH、GFS、GPFS、Swift などのどれが適していますか?

[[268507]]コンテナ クラウドで分散ストレージを使用する場合、HDFS、CEPH、GFS、G...

虎牙闘魚は「互いに共感し合う」

4月7日、テンセントのモバイルeスポーツコンテンツプラットフォーム「Penguin E-Sports...

#オランダサーバー# hostcircle: €949、2*AMD Epyc Milan7713 (128 コア/256 スレッド)、1T メモリ、3.84T NVMe、10Gbps 帯域幅

hostcircle は現在、オランダのアムステルダム データ センターで、高負荷タスクの実行に最適...

社内ブランドマーケティングは数え切れないほど多くの人々に無視されています。

この記事を読み始める前に、ちょっとしたテストをしてみましょう。画像ソース: The Paperなぜこ...

外部リンクレイアウトの新しい基準: 外部リンクを構築すべきでないウェブサイト

今日、私はa5のウェブサイトでBaiduが発表したメッセージを見ました。このメッセージは外部リンクに...