プロジェクトの実践を継続し、Redis分散ロックを統合する

プロジェクトの実践を継続し、Redis分散ロックを統合する

[[386548]]

今日は、Redis ベースの分散ロックをプロジェクトに統合します。分散ロックは常に皆の注目を集めてきました。仕事でも面接でも、分散ロックは常に話題になります。誰もがこのスキルを習得して、将来昇進したりお金を稼いだりできるようになることを願っています。

分散ロック

背景

分散ロックはなぜ必要なのでしょうか? synchronized、ReantrantLock、その他の関連ロックはすでに存在していませんか?

はい、アプリケーションを開発するときに、共有変数へのマルチスレッド アクセスを同期する必要がある場合、学習したロックを使用してそれを処理することができ、バグなしで完璧に実行できます。

注: これはスタンドアロン アプリケーションです。ビジネスが発展するにつれて、クラスタリングが必要になります。アプリケーションを複数のマシンにデプロイし、負荷分散する必要があります。

上図からわかるように、変数 A は 3 つのサーバーのメモリ内に存在します (この変数 A は主にクラス内のメンバー変数として反映され、ステートフル オブジェクトです)。制御が追加されない場合、変数 A は同時にメモリの一部を割り当てます。この変数を操作するために 3 つのリクエストが同時に送信されます。明らかに、結果は間違っています!同時に送信されない場合でも、3 つの要求はそれぞれ 3 つの異なるメモリ領域のデータに対して操作を行います。変数A間で共有がなく、可視性もないので、処理結果も間違っています!

私たちのビジネスにこのようなシナリオが存在する場合、この問題を解決する方法が必要です。

同時実行性の高い状況でメソッドまたはプロパティが同じスレッドによってのみ同時に実行されるようにするために、単一アプリケーションの従来の単一マシン展開の場合、同時処理関連の関数を使用して相互排他制御を行うことができます。しかし、ビジネス ニーズの進化に伴い、当初の単一マシン展開システムは分散クラスター システムへと進化しました。分散システムはマルチスレッド、マルチプロセスであり、異なるマシンに分散されているため、元の単一マシン展開の同時実行制御ロック戦略は無効になります。単純なアプリケーションでは分散ロック機能を提供できません。この問題を解決するには、共有リソースへのアクセスを制御するマシン間の排他制御メカニズムが必要です。これは分散ロックが解決する必要がある問題です。

分散ロックの基本原理

分散環境において、複数のマシン上の複数のプロセスが同じ共有リソース(データ、ファイルなど)を操作する場合、相互排他制御が行われないと、残高がマイナスになったり、商品が売れ過ぎたりすることがあります。

この問題を解決するには、分散ロック サービスが必要です。まず、分散ロックに必要な条件を見てみましょう。

  • 相互排他性: 常に同じロックを保持できるのは 1 つのクライアントのみであるため、共有リソースは一度に 1 つのクライアントによってのみ操作されます。
  • セキュリティ: デッドロックは発生しません。クライアントがロックを保持しているときにクラッシュし、ロックをアクティブにロック解除しない場合、保持しているロックは正しく解放され、他のクライアントが後でロックできるようになります。
  • 可用性: ロック サービスを提供するノードでダウンタイムなどの回復不可能な障害が発生した場合、「ホット スタンバイ」ノードが障害が発生したノードを引き継いでサービスの提供を継続し、保持するデータが障害が発生したノードと一致していることを保証します。
  • 対称性: どのロックでも、ロックとロック解除は同じクライアントによって実行される必要があります。つまり、クライアント A はクライアント B によって追加されたロックを解除できません。

現在、市場には 3 種類の分散ロック実装ソリューションが存在します。

  1. データベースの楽観的ロック。
  2. 分散キャッシュに基づくロック サービス (通常は Redis と Redis に基づく RedLock)。
  3. 分散コンセンサス アルゴリズムに基づいて実装されたロック サービスには、ZooKeeper、Chubby、ETCD などがあります。

プロジェクトでは、後者の 2 つが最もよく使用されます。実際、それぞれのソリューションには独自の長所と短所があります。

デッドロック防止

次の典型的なデッドロックのシナリオを見てみましょう。

クライアントはロックを正常に取得しましたが、ロックを解放する前にクラッシュしました。この時点で、クライアントは実際にパブリック リソースを操作する権限を失っていますが、ロック解除 (キーと値のペアの削除) を要求する方法はありません。すると、常にロックが保持され、他のクライアントはロックを取得できなくなります。

私たちの解決策は、ロックするときにロックの有効期限を設定することです。有効期限に達すると、Redis はデッドロックを回避するために、対応するキー値を自動的に削除します。この有効期限は、ロック保有者が有効期限内に関連する操作を完了し、ロックを解除できることを保証するために、特定のビジネスの包括的な評価と組み合わせて設定する必要があることに注意してください。

また、前述の通り、実際のプロジェクトでは後者の 2 つのソリューションを使用しているため、後者の 2 つのソリューションに重点を置いています。この記事は、私たちが以前実施したプロジェクトに基づいていることに注意してください。同時に、Redis がプロジェクトに統合されたため、この記事の分散ロックは Redis の実装に基づいています。

Redis ベースの分散ロックの実装

まず、Redis を使用してソリューションを実装するというアイデアについて説明します。

  • setnx +有効期限+削除
  • setnx+lua
  • キー値 px ミリ秒 nx を設定

シンプルバージョン

分散ロックの作成

  1. org.slf4j.Logger をインポートします。
  2. org.slf4j.LoggerFactory をインポートします。
  3. org.springframework.data.redis.core.RedisTemplate をインポートします。
  4. org.springframework.stereotype.Component をインポートします。
  5.  
  6. javax.annotation.Resource をインポートします。
  7. java.util.UUID をインポートします。
  8. java.util.concurrent.TimeUnitをインポートします。
  9.  
  10. @成分
  11. パブリッククラスDistributedLockV1 {
  12.  
  13. プライベート Logger ロガー = LoggerFactory.getLogger(DistributedLockV1.class);
  14.  
  15. @リソース
  16. プライベート Redis テンプレート redisTemplate;
  17.  
  18. パブリックブールロック(文字列ビジネスキー) {
  19. ブール値の結果 = false ;
  20. 文字列 uniqueValue = UUID.randomUUID().toString();
  21. 試す {
  22. //@see <a href= "http://redis.io/commands/setnx" > Redis ドキュメント: SETNX</a>
  23. 結果 = redisTemplate.opsForValue().setIfAbsent(businessKey, uniqueValue);
  24. if (!結果) {
  25. 戻る 間違い;
  26. }
  27. //キーの有効期間を設定する
  28. redisTemplate.expire(businessKey、10、TimeUnit.SECONDS);
  29. 結果を返します
  30. } catch (例外例) {
  31. logger.error( "ロックの取得に失敗しました" , ex);
  32. }
  33. 結果を返します
  34. }
  35.  
  36. パブリックvoid ロック解除(String businessKey) {
  37. 試す {
  38. //消去 
  39. redisTemplate.delete (ビジネスキー) ;
  40. } catch (例外例) {
  41. logger.error( "ロックの解放に失敗しました" , ex);
  42. }
  43. }
  44. }

このようにして、単純な分散ロックが実装されますが、ここで問題が発生します。問題は必ずしも発生するとは限りませんが、ある瞬間には発生します。

次に、この分散ロックをユーザーアカウント残高を差し引く機能に適用します。

主に userId と balance を含むユーザー アカウント テーブルを作成しましょう。

  1. 作成する テーブル`user_account` (
  2. `id`ビッグインテンシブ ない  NULL AUTO_INCREMENT、
  3. `user_id`ビッグ整数 デフォルト  NULL
  4. `balance`小数点(10,2)デフォルト  NULL
  5. `create_time`タイムスタンプ  NULL  デフォルト  NULL  の上 アップデート  CURRENT_TIMESTAMP
  6. `update_time`タイムスタンプ  NULL  デフォルト  NULL  の上 アップデート  CURRENT_TIMESTAMP
  7. 主要な キー(`id`)
  8. ) ENGINE=InnoDB AUTO_INCREMENT=2デフォルトCHARSET=utf8;

次に、UserAccountRepository インターフェースを作成します。

  1. com.tian.user .entity.UserAccountをインポートします
  2. org.springframework.data.jpa.repository.JpaRepository をインポートします。
  3. org.springframework.data.jpa.repository.Modifying をインポートします。
  4. org.springframework.data.jpa.repository.Query をインポートします。
  5. org.springframework.stereotype.Repository をインポートします。
  6.  
  7. java.math.BigDecimal をインポートします。
  8.  
  9. @リポジトリ
  10. パブリックインターフェース UserAccountRepository は JpaRepository<UserAccount, Long> を拡張します {
  11.  
  12. ユーザーアカウント findByUserId(Long userId);
  13. // ユーザーIDで残高を更新
  14. @変更中
  15. @Query( "UserAccount u を更新し、u.balance=?1 を設定し、u.userId=?2 を設定します" )
  16. void updateBalanceByUserId(BigDecimal 残高、Long ユーザー ID);
  17. }

UserAccountService と実装クラスを作成し、その推論メソッドを実装します。

  1. com.tian.user .entity.UserAccountをインポートします
  2.  
  3. java.math.BigDecimal をインポートします。
  4.  
  5. パブリックインターフェースUserAccountService{
  6. /**
  7. * 残高を差し引く
  8. * @param userId 現在のユーザーのユーザーID
  9. * @param balance 現在の残高を減らす必要がある
  10. * @return控除が成功したかどうか
  11. */
  12. boolean バランスを戻す(Long ユーザー ID、BigDecimal 残高);
  13. }
  14. com.tian.user .entity.UserAccount をインポートします。
  15. com.tian.user .lock.DistributedLockV1 をインポートします。
  16. com.tian.user .repository.UserAccountRepository をインポートします。
  17. com.tian.user.service.UserAccountServiceをインポートします
  18. org.springframework.stereotype.Service をインポートします。
  19. org.springframework.transaction.annotation.Transactionalをインポートします
  20.  
  21. javax.annotation.Resource をインポートします。
  22. java.math.BigDecimal をインポートします。
  23. java.util.Dateをインポートします
  24.  
  25. @サービス
  26. @Transactional(ロールバックFor = Exception.class)
  27. パブリッククラス UserAccountServiceImpl は UserAccountService を実装します {
  28. プライベート Logger logger = LoggerFactory.getLogger(getClass());
  29. @リソース
  30. プライベート UserAccountRepository userAccountRepository;
  31. @リソース
  32. プライベートDistributedLockV1distributedLockV1;
  33.      
  34. @オーバーライド
  35. パブリックブール値reduceBalance(Long userId, BigDecimal balance) {
  36. 試す {
  37. // アカウントをロックし、userId をキーとして使用します
  38. ブール型ロック = 分散LockV1.lock(userId.toString());
  39. //ロック取得に失敗した場合は、直接推論失敗が返されます
  40. (!ロック)の場合{
  41. 戻る 間違い;
  42. }
  43. ユーザーアカウント userAccount = userAccountRepository.findByUserId(userId);
  44. BigDecimal currBalance = userAccount.getBalance();
  45. // 残高が控除に十分かどうかを確認します
  46. (currBalance.compareTo(残高) > 0)の場合{
  47. BigDecimal newBalance = currBalance.subtract(残高);
  48. //残高を差し引く
  49. userAccountRepository.updateBalanceByUserId(新しいバランス、ユーザーID);
  50. 戻る 真実;
  51. }
  52. }catch(例外ex){
  53. logger.error( "残高控除に失敗しました" , ex);
  54. ついに
  55. //ロックを解除する
  56. 分散ロックV1.ロック解除(userId.toString());
  57. }
  58. 戻る 間違い;
  59. }
  60. }

この時点で、分散ロックの簡易バージョンとその使用方法は完了です。以下のアイデアを整理してみましょう。

1. setnx (set not existing) を使用します。これは、設定されたキーが Redis に存在しない場合は true を返し、存在する場合は false を返すことを意味します。

2. 設定したキーの有効期間を設定し、expire を使用して有効期間を設定します。

3. 控除に利用可能な残高が十分かどうかを確認します。そうでない場合は、プロセスを直接終了し、delete を使用して Redis 内のキーを削除します。

4. 残高を差し引いて、データベースの残高値を更新します。

5. キーを削除します。Redis のキーを削除します。

それで、ここで疑問が生じます。最初のステップと 2 番目のステップは両方とも成功しました。しかし、残高を照会して残高を差し引く 3 番目のステップに 20 秒かかる場合、上記の Redis のキーの有効期間として設定した 10 秒は、タイムアウトになり、キーの有効期限が切れ、10 秒から 20 秒の間に他のスレッドがロックを取得したことを意味します。その後、他のスレッドによって取得されたロックは削除され、他のスレッドのロックは解放されます。今は混乱していませんか?

これは面接でもよく聞かれる質問です。分散ロックに Redis を使用しているときにビジネスがタイムアウトになった場合はどうすればいいでしょうか?

アップグレード版

各キーに対応する値を返し、ロックが解除されたときに、現在のキーに対応する値が現在保持されている値であるかどうかを判断できます。

その後、上記のバージョンを修正しました。

  1. org.slf4j.Logger をインポートします。
  2. org.slf4j.LoggerFactory をインポートします。
  3. org.springframework.data.redis.core.RedisTemplate をインポートします。
  4. org.springframework.stereotype.Component をインポートします。
  5.  
  6. javax.annotation.Resource をインポートします。
  7. java.util.UUID をインポートします。
  8. java.util.concurrent.TimeUnitをインポートします。
  9.  
  10. @成分
  11. パブリッククラスDistributedLockV1 {
  12. プライベート Logger ロガー = LoggerFactory.getLogger(DistributedLockV1.class);
  13. @リソース
  14. プライベート Redis テンプレート redisTemplate;
  15.  
  16. パブリック文字列 lockV2(文字列ビジネスキー) {
  17. ブール値の結果 = false ;
  18. 文字列 uniqueValue = UUID.randomUUID().toString();
  19. 試す {
  20. 結果 = redisTemplate.opsForValue().setIfAbsent(businessKey, uniqueValue);
  21. if (!結果) {
  22. 戻る ヌル;
  23. }
  24. redisTemplate.expire(businessKey、100、TimeUnit.SECONDS);
  25. 一意の値を返します
  26. } catch (例外例) {
  27. logger.error( "ロックの取得に失敗しました" , ex);
  28. }
  29. 戻る ヌル;
  30. }
  31.  
  32. パブリックvoid unlockV2(文字列ビジネスキー、文字列ビジネス値) {
  33. 試す {
  34. オブジェクト値 = redisTemplate.opsForValue().get(businessKey);
  35. 値 == null場合
  36. 戻る 間違い;
  37. }
  38. //Redis の現在のキーの値は、現在のスレッドによって保持されている値と一致していますか?
  39. if (!businessValue.equals(値)) {
  40. //矛盾しており、他のスレッドによって取得されたことを証明している
  41. logger.info( "key={}ロックの解放に失敗しました。ロックは他のスレッドによって取得されています" , businessKey);
  42. 戻る 間違い;
  43. }
  44. redisTemplate.delete (ビジネスキー) ;
  45. logger.info( "key={} ロックが正常に解放されました" , businessKey);
  46. } catch (例外例) {
  47. logger.error( "ロックの解放に失敗しました" , ex);
  48. }
  49. }
  50. }

ここでは、ロックを保持しているスレッドが現在のスレッドであるかどうかを判断するという、単純なバージョンよりももう 1 つの判断があります。現在のスレッドであるかどうかを判断するためにランダム文字列の値が使用されますが、ロックを解除するときにアトミック操作を実行することは依然として不可能です (delete メソッド)。たとえば、プロセス A がビジネス ロジックの実行を終了し、ロックを解放しようとすると、プロセス A のロックは自動的に期限切れになり、別のプロセス B がロックを正常に取得します。 B が実行する前に、プロセス A が delete(key) を実行し、プロセス B のロックを解除します。そのため、ロックを解除するには Lua スクリプトと連携する必要があります。

setnx+Lua スクリプト Lua は、標準 C 言語で記述され、ソース コード形式で公開されている軽量でコンパクトなスクリプト言語です。アプリケーションに組み込むように設計されており、アプリケーションの柔軟な拡張性とカスタマイズ機能を提供します。

Lua は対話型プログラミング モードを提供します。コマンドラインでプログラムを入力すると、その効果がすぐにわかります。

Lua スクリプトの利点:

  • ネットワーク オーバーヘッドを削減: 複数のリクエストの元のロジックは Redis サーバー上で完了します。スクリプトを使用してネットワークの往復遅延を減らす
  • アトミック操作: Redis はスクリプト全体をまとめて実行し、途中で他のコマンドは挿入されません (トランザクションと考えてください)
  • 再利用: クライアントから送信されたスクリプトはRedisに永続的に保存されるため、他のクライアントはコードを使用せずにこのスクリプトを再利用できるため、同じロジックを完了できます。

リソース ディレクトリに redis-lock.lua ファイルを作成します。内容を入力してください:

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

このコードは何を意味するのでしょうか。つまり、キーを通じて Redis の値を取得し、それを現在のスレッドの値と比較します。同じ場合は、Redis 内のキーを削除します。削除された場合は 1 を返し、それ以外の場合は何も行われなかったことを示す 0 を返します。

Redis ロック コード ブロックは次のとおりです。

  1. org.slf4j.Logger をインポートします。
  2. org.slf4j.LoggerFactory をインポートします。
  3. org.springframework.core.io.ClassPathResource をインポートします。
  4. org.springframework.data.redis.core.RedisTemplate をインポートします。
  5. org.springframework.data.redis.core.script.DefaultRedisScript をインポートします。
  6. org.springframework.scripting.support.ResourceScriptSource をインポートします。
  7. org.springframework.stereotype.Component をインポートします。
  8.  
  9. javax.annotation.PostConstruct をインポートします。
  10. javax.annotation.Resource をインポートします。
  11. java.util.ArrayList をインポートします。
  12. java.util.List をインポートします。
  13. java.util.UUID をインポートします。
  14. java.util.concurrent.TimeUnitをインポートします。
  15.  
  16. @成分
  17. パブリッククラスDistributedLockV1 {
  18.  
  19. プライベート Logger ロガー = LoggerFactory.getLogger(DistributedLockV1.class);
  20.  
  21. @リソース
  22. プライベート Redis テンプレート redisTemplate;
  23.  
  24. プライベート DefaultRedisScript<Long> スクリプト;
  25.  
  26. @投稿コンストラクト
  27. パブリックvoid init() {
  28. スクリプト = 新しい DefaultRedisScript<Long>();
  29. スクリプトでResultTypeを設定します。
  30. script.setScriptSource(新しい ResourceScriptSource(新しい ClassPathResource( "redis-lock.lua" )));
  31. }
  32.  
  33. パブリック文字列 lockV3(文字列キー) {
  34. 文字列値 = UUID.randomUUID().toString()。交換する "-" "" );
  35.  
  36. /*
  37. * setIfAbsent <=> SET  キー値 [NX] [XX] [EX <秒>] [PX [ミリ秒]]
  38. *有効期限を5分に設定
  39. */
  40. ブール フラグ = redisTemplate.opsForValue().setIfAbsent(キー、値、10000、TimeUnit.MILLISECONDS);
  41. if (フラグ) {
  42. 戻り値;
  43. }
  44. 戻る ヌル;
  45. }
  46.  
  47. パブリックvoid unlockV3(文字列キー、文字列値) {
  48. /** ビジネスロジックの処理が完了したので、ロックを解除します **/
  49. 文字列 lockValue = (文字列) redisTemplate.opsForValue().get(キー);
  50. lockValue != null && lockValue.equals(value) の場合 {
  51. システム。 out .println( "lockValue========:" + lockValue);
  52. リスト<String>キー = 新しいArrayList<>();
  53. keys.add (キー);
  54. オブジェクト実行= redisTemplate。実行(スクリプト、キー、lockValue)
  55. システム。 out .println( "execute 実行結果、1 は del が実行されたことを意味し、0 は実行されなかったことを意味します ===== " + execute );
  56. logger.info( "{} ロック解除に成功しました。業務処理を終了しました" , key );
  57. 戻る;
  58. }
  59. logger.info( "key={} ロックの解放に失敗しました" , key );
  60. }
  61. }

最後に、罰金の控除を再度実行すると、ログ出力は次のようになります。

  1. ロック値========:199740e62c184a6a9897f9c95e720b4d
  2. 実行結果を実行します。1 は del が実行されたことを意味し、0 は実行されなかったことを意味します ===== 1
  3. 2021-03-09 19:03:51.592 INFO 6692 --- [nio-8080-exec-4] com.tian.user.lock.DistributedLockV1 : 1 正常にロック解除されました。処理業務は終了しました 

これまで、setnx+Lua ソリューションを実装してきました。これについて疑問があるなら、それは良いことです。問題が発生しないかどうかを確認するために、罰金控除サービス メソッドを呼び出す複数のスレッドを作成することをお勧めします。

"知らせ"

setnx は redis の以前のバージョンには存在せず、後で導入されました。実際、set コマンドを使用して setnx 問題を解決することもできます。さらに、有効期限を追加することもできます。全体的なコマンドは

  1. セット キー値 nx px xxx

値はランダムな文字列であることが望ましいです。これにより、ビジネス コードの実行時間が設定されたロックの自動有効期限を超え、ロックが再度解除されたときに他のプロセス ロックが解除されることを防ぐことができます。

setnx lock の最大の欠点は、ロック時に 1 つの Redis ノードでのみ機能することです。 Redis が Sentinel を使用して高可用性を確保している場合でも、何らかの理由でマスター ノードがマスターからスレーブに切り替わると、ロックは失われます。次に例を示します。

  1. ロックは Redis マスター ノードで取得されます。
  2. しかし、このロックされたキーはまだスレーブ ノードに同期されていません。
  3. マスターに障害が発生すると、フェイルオーバーが発生し、スレーブ ノードがマスター ノードにアップグレードされます。
  4. 上記のマスターノードのロックが失われました。

時々、単に鍵を紛失するだけという単純な問題ではないことがあります。新しく選出されたマスター ノードは同じロックを再度取得できるため、ロックが 2 回取得されるシナリオが発生します。

このことから、ロックが 2 回取得されると、セキュリティが確実に満たされないことがわかります。

最初の 2 つのソリューションはあまり満足できるものではなく、常に何らかの問題を抱えていますが、一部の企業では採用されています。次に、Redis に基づく分散ロックのより高度なバージョンを見てみましょう。

高度なRedisson + RedLock

Redisson は Java 用の Redis クライアントの 1 つであり、Java で分散ロックを実装するために Redis 公式 Web サイトによって推奨されているプロジェクトです。

Redisson は、Redis の操作を容易にするためのいくつかの API を提供します。この記事は主にロックについて書かれているため、ロック関連のクラスに焦点を当てます。 Redisson が提供するさまざまなロックは次のとおりです。

  • 再入可能ロック
  • フェアロック
  • インターロック(マルチロック)
  • レッドロック
  • 読み取り書き込みロック
  • セマフォなど

つまり、理解しているかどうかにかかわらず、Redisson は一連のロックを提供します...これは、ほとんどの企業が Redis 分散ロックを使用する最も一般的な方法でもあります。

ロックおよびロック解除コードの全体的な構造は次のとおりです。

  1. RLock ロック = redissonClient.getLock( "xxx" );
  2.  
  3. ロック。ロック();
  4.  
  5. 試す {
  6. ...
  7. ついに
  8. ロックを解除します。
  9. }

実際、磁気層のロックとロック解除も Lua スクリプトを使用して実装されています。興味のある友人は、デバイスの基盤となるソースコードを調べることができます。

スペースの制約により、Redis クラスターも関係するため、ロックとロック解除のフローチャートは参考としてのみここに示します。

「ロック処理」

ロック解除プロセス

要約する

Redis の高可用性を実現するために、通常は、Redis ノードに 1 つ以上のスレーブ ノードをマウントし、マスターとスレーブの切り替えにセンチネル モードを使用するなど、Redis クラスター モードを構築します。ただし、Redis のマスター スレーブ モードは非同期であるため、データ同期中にマスター ノードがクラッシュし、データ同期に時間をかける前にスレーブ ノードがマスター ノードとして選出され、データが失われる可能性があります。一般的なプロセスは次のとおりです。

  1. ユーザーは Redis マスター ノードのロックを取得します。
  2. マスターノードがダウンしており、ストレージロックのキーがスレーブノードにまだ同期されていません。
  3. スレーブ ノードがマスター ノードにアップグレードされます。
  4. ユーザーは、新しいマスター ノードから同じリソースに対応するロックを取得し、同じロックを 2 回取得しました。

さて、この問題を解決するために、Redis の作者は RedLock アルゴリズムを提案しました。手順は次のとおりです (5 つのステップ)。

次の例では、同時にクラッシュしないように、それぞれ 5 台のサーバー上で実行される 5 つの完全に独立した Redis マスター ノードがあると想定しています。

現在の Unix 時間をミリ秒単位で取得します。

同じキーとランダム値を使用して、N 個のインスタンスから順番にロックを取得しようとします。ステップ 2 で、Redis にロックを設定する場合、クライアントは、ロックの有効期限よりも短いネットワーク接続と応答のタイムアウトを設定する必要があります。たとえば、ロックの自動有効期限が 10 秒の場合、タイムアウトは 5 ~ 50 ミリ秒にする必要があります。これにより、サーバー側の Redis がすでにクラッシュしているにもかかわらず、クライアントが応答結果を待機している状況を回避できます。サーバーが指定された時間内に応答しない場合、クライアントはできるだけ早く別の Redis インスタンスを試す必要があります。

クライアントは、現在の時刻からロックが取得された時刻 (手順 1 で記録された時刻) を引いて、ロックを取得するために使用された時刻を取得します。ロックが Redis ノードの過半数 (ここでは 3 つのノード) から取得され、使用された時間がロックの有効期限よりも短い場合にのみ、ロックが正常に取得されたと見なされます。

ロックが取得された場合、キーの実際の有効期間は、有効期間からロックの取得に使用された時間を差し引いたものになります (手順 3 で計算された結果)。

何らかの理由でロックの取得に失敗した場合 (少なくとも N/2+1 個の Redis インスタンスでロックが取得されていないか、ロックの取得時間が有効時間を超えている場合)、クライアントはすべての Redis インスタンスのロックを解除する必要があります (一部の Redis インスタンスが正常にロックされていない場合でも)。

この時点で、基本的に、ほとんどの Redis ノードが正常に動作できる限り、Redlock の正常な動作が保証されることがわかります。これにより、これまでのシングルポイント Redis の場合に発生していた非同期通信によるノード障害やロック障害の問題を解決できます。

しかし、慎重に検討した結果、Redlock には依然として以下の問題が残っています。

合計 5 つの Redis ノード (A、B、C、D、E) があると仮定します。次の一連のイベントを想像してください。

  1. クライアント 1 は A、B、C を正常にロックし、ロックを正常に取得しました (ただし、D と E はロックされませんでした)。
  2. ノード C がクラッシュして再起動しましたが、クライアント 1 によって C に追加されたロックは保持されず、失われました。
  3. ノード C が再起動すると、クライアント 2 は C、D、および E をロックし、ロックを正常に取得します。
  4. したがって、クライアント 1 とクライアント 2 は同時に (同じリソースに対して) ロックを取得します。

残念ながら、障害再起動によって引き起こされるロック セキュリティの問題はまだ解決できません...

ノードの再起動によって発生するロック失敗の問題に対応して、Redis の作者は遅延再起動の概念を提案しました。基本的に、ノードがクラッシュした後はすぐに再起動せず、一定時間待ってから再起動します。待機時間はロックの有効期限よりも長くする必要があります。この方法では、再起動前にノードに関係するすべてのロックが期限切れになっていることが保証されます。再起動を遅らせるとこの問題は解決するようです...

しかし、まだ問題が残っています。ノードが再起動された後、待機期間中は外部的には動作しません。その後、ほとんどのノードがダウンして待機状態になると、有効期限内にロックを正常にロックできないため、システムは使用できなくなります。

つまり、Redis 分散ロックの実装には解決すべき問題がまだ多く残っています。これらの問題を認識し、Redis 分散ロックを正しく実装する方法を理解した上で、作業時に分散ロックを合理的に選択して正しく使用する必要があります。

しかし、なぜ一部の企業は分散ロックを実装するために Redis を使用するのでしょうか?実際、分散ロックを実装する場合、ミドルウェアから Zookeeper を選択することもできます。Zookeeper は Redis よりもはるかに信頼性が高いですが、効率は少し低くなります。同時実行性がそれほど大きくなく、信頼性が重要な場合は、Zookeeper が間違いなく第一の選択肢です。

分散ロックの実装に関しては、絶対的に良いとか悪いとかはなく、最善の解決策はなく、ビジネスに最適な解決策があるだけです。

次の 2 つの解決策は参考用です。

  • 効率を重視する場合は、Redis を使用して実装することをお勧めします。
  • 信頼性を重視する場合は、Zookeeper を使用することをお勧めします。

この記事はWeChat公開アカウント「Java Backend Technology Full Stack」から転載したものです。以下のQRコードからフォローできます。この記事を転載する場合は、Java Backend Technology Full Stack パブリック アカウントにお問い合わせください。

<<:  IBM Cloud Pak for Automation が ENN グループのハイパーオートメーション化を支援

>>:  エッジ コンピューティングとクラウド コンピューティング: どちらがより効率的ですか?

推薦する

2013 年にウェブサイトを Baidu のホームページに掲載する方法 - オンサイト最適化

2012年8月23日はウェブマスターにとって哀悼の日です。この日、Baiduが検索エンジンを調整し、...

liteserver、15周年、すべてのオランダのVPSが15%オフ、月額5.1ユーロから、2Gメモリ/2コア/40g NVMe/15Tトラフィック

オランダの老舗「LiteServer BV」が創業15周年を迎えました。これを記念して、NVMe S...

Virmach-50% オフ/Windows/ハイエンド VPS/サンノゼ/フェニックス/10 データセンター

virmach は、OVZ と Windows、純粋な SSD ハード ドライブを含む、通常価格の ...

エッジ コンピューティングとクラウド コンピューティング: 企業の接続デバイスにはどちらのソリューションが適していますか?

企業が何を構築しているかに関係なく、ある時点で、デバイスは重要な計算をクラウドで実行するべきか、それ...

知福電子は決済規則違反で2640万元の罰金を科せられた

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています最近、中国...

高速かつ低価格が求められる古いアメリカの VPS をいくつかお勧めします。

安価なアメリカVPSや高速なアメリカVPSなど、アメリカVPS業者は数多く存在しますが、長期的に運用...

4つの分散データベースシナリオの選択、利点と欠点の比較分析、および将来の展望

[[273746]] 1 はじめに近年、国際的な情報セキュリティ情勢が厳しさを増す中、国家の情報セ...

半分の労力で2倍の結果を得るための最適化方法

世の中にはさまざまなタイプのウェブサイトがあります。どのようなタイプのウェブサイトであっても、オンサ...

raksmart: 米国のクラスター サーバー、無制限のトラフィック サーバー (1G~10Gbps の帯域幅)、わずか 197 ドルから

raksmartは8月から今月15日まで、西海岸(サンノゼ)の独立データセンターで、253IPクラス...

業界ウェブサイトの収益方法分析:教育ウェブサイト

セグメント化された業界ウェブサイトは、ビジネスを始めたり、独自のウェブサイトを構築したりするための方...

中小企業におけるクラウドコンピューティング導入の3つの大きな障害

クラウド コンピューティング テクノロジーがデジタル変革に利用されていることは周知の事実です。実際、...

中国電子クラウド徐州ノードが正式に開始され、徐州のデジタル化に貢献

2021年6月23日、徐州市人民政府が主催する「2021年中国(徐州)第5回人工知能会議及びデジタル...

ゲーミフィケーション運用戦略の事例分析!

この記事は、ゲームの種類と特徴を分析することで、ユーザーのモチベーションを高めるゲーミフィケーション...