Redis分散ロックの実装

Redis分散ロックの実装

[[335642]]

序文

日々の開発では、ロックが必要な状況に必然的に遭遇します。たとえば、製品在庫を減算するには、まずデータベースから在庫を取得し、在庫決定を実行してから在庫を減算する必要があります。この一連の操作は明らかにアトミック性に準拠していません。コード ブロックがロックされていない場合、同時実行による過剰販売の問題が発生する可能性が高くなります。システムがモノリシック アーキテクチャである場合は、ローカル ロックを使用することで問題を解決できます。分散アーキテクチャの場合は、分散ロックが必要です。

プラン

SETNXコマンドとEXPIREコマンドの使用

  1. SETNXキー値 
  2. EXPIRE キー秒数 
  3. DELキー 
  4. (setnx("item_1_lock", 1)) の場合 {  
  5. 有効期限が切れます("item_1_lock", 30);  
  6. 試す {  
  7. ... ロジック 
  8. } キャッチ {  
  9. ...  
  10. ついに 
  11. del("item_1_lock");  
  12. }  
  13. }

この方法は問題を解決するように見えますが、SETNX および EXPIRE 操作は非アトミックであるため、一定のリスクがあります。 SETNX が成功した後にエラーが発生した場合、ロックにタイムアウト期間がないため、EXPIRE は実行されず、デッドロックが発生します。

この場合、Lua スクリプトを使用して操作のアトミック性を維持し、SETNX 操作と EXPIRE 操作の両方が成功または失敗することを確認できます。

  1. if (redis.call('setnx', KEYS[1], ARGV[1]) <   1 )  
  2. 0 を返します。  
  3. 終わり;  
  4. redis.call('expire', KEYS[1], tonumber(ARGV[2]));  
  5. 1 を返します。

この方法により、競合するロックの原子性の問題を暫定的に解決しました。他の機能はまだ実装されていませんが、デッドロックは発生しないはずです🤪🤪🤪。

Redis 2.6.12以降ではSETコマンドを柔軟に使用できます

  1. キー値の設定 NX EX 30  
  2. DELキー 
  3. if (set("item_1_lock", 1, "NX", "EX", 30)) {  
  4. 試す {  
  5. ... ロジック 
  6. } キャッチ {  
  7. ...  
  8. ついに 
  9. del("item_1_lock");  
  10. }  
  11. }

改良された方法は、Lua スクリプトを使用せずに SETNX および EXPIRE の原子性問題を解決します。では、よく考えてみましょう。 A がロックを取得し、コード ブロックに正常に入力してロジックを実行したが、さまざまな理由によりタイムアウトになり、ロックが自動的に解放された場合。その後、B はロックを正常に取得し、コード ブロックに入ってロジックを実行します。ただし、A がロジック実行を完了した後にロックを解除すると、B が取得したばかりのロックも解除されます。それは自分の鍵を使って他人のドアを開けるようなもので、許されることではありません。

この問題を解決するには、SET 時にロック フラグを設定し、DEL 時に現在のロックが自分のロックであるかどうかを確認します。

  1. 文字列= UUID .randomUUID().toString().replaceAll("-", "");  
  2. if (set("item_1_lock", 値, "NX", "EX", 30)) {  
  3. 試す {  
  4. ... ロジック 
  5. } キャッチ {  
  6. ...  
  7. ついに 
  8. ... lua スクリプトはアトミック性を保証します 
  9. }  
  10. }  
  11. redis.call('get', KEYS[1]) == ARGV[1]の場合 
  12. その後、redis.call('del', KEYS[1]) を返します。  
  13. それ以外の場合は0を返す 
  14. 終わり

この時点で、競合するロックの原子性の問題と、誤ってロックを削除する問題がようやく解決されました。ただし、ロックは通常、再入、循環待機、タイムアウト時の自動更新などの機能もサポートする必要があります。次に、これらの問題を解決するために非常に便利なパッケージの使用方法を学びます。

Redissonを使い始める

Redission のロックは、再入可能およびタイムアウトの自動更新機能を実装しており、これらはすべてカプセル化されています。上記のいくつかの機能ポイントを簡単に実装するには、必要に応じて API を呼び出すだけです。詳細な機能については、Redissonのドキュメントを参照してください。

プロジェクトにRedissonをインストールする

  1. <依存関係>    
  2. <グループID> org.redisson</グループID>    
  3. <artifactId>再配布</artifactId>    
  4. <バージョン> 3.13.2</バージョン>    
  5. </依存関係>  
  1. 実装 'org.redisson:redisson:3.13.2'

Maven または Gradle を使用してビルドします。最新バージョンは3.13.2です。必要なバージョンは、Redisson でも見つかります。

単純な試み

  1. RedissonClient redissonClient = Redisson .create();  
  2. RLockロック= redissonClient .getLock("lock");  
  3. ブール値res = lock .lock();  
  4. もし(res){  
  5. 試す {  
  6. ... ロジック 
  7. ついに 
  8. ロックを解除します。  
  9. }  
  10. }

Redisson は基礎となるロジックをすべてカプセル化します。具体的な実装については気にする必要はありません。ほんの数行のコードで完璧なロックを使用できます。次に、ソースコードを簡単にいじってみましょう🤔🤔🤔。

ロック

  1. private void lock(longleasingTime, TimeUnit unit, boolean interruptibly) は InterruptedException をスローします {  
  2. 長いthreadId =スレッド.currentThread().getId();  
  3. Long ttl = tryAcquire (leaseTime、unit、threadId);  
  4. ttl == nullの場合{
  5.   戻る;  
  6. }  
  7. RFuture < RedissonLockEntry >  将来=サブスクライブ(スレッド ID);  
  8. if (割り込み可能) {  
  9. コマンドExecutor.syncSubscriptionInterrupted(将来);  
  10. } それ以外 {  
  11. コマンドExecutor.syncSubscription(将来);
  12. }  
  13. 試す {  
  14. (真)の間{  
  15. ttl = tryAcquire (リース時間、ユニット、スレッド ID);  
  16. ttl == nullの場合{  
  17. 壊す;  
  18. }  
  19. ttl > = 0 の場合 
  20. 試す {  
  21. future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);  
  22. } キャッチ (InterruptedException e) {  
  23. if (割り込み可能) {  
  24. eを投げる;  
  25. }  
  26. future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);  
  27. }  
  28. } それ以外 {  
  29. if (割り込み可能) {  
  30. 将来。getNow()。getLatch()。取得();  
  31. } それ以外 {  
  32. future.getNow().getLatch().acquireUninterruptibly();  
  33. }  
  34. }
  35. }  
  36. ついに 
  37. 購読を解除します(将来、スレッドID);  
  38. }  
  39. }

ロックを取得

  1. プライベート< T > RFuture < Long > tryAcquireAsync(long リースタイム、TimeUnit ユニット、long スレッド ID) {  
  2. リースタイムが -1 の場合 
  3. tryLockInnerAsync(leaseTime、ユニット、スレッドId、RedisCommands.EVAL_LONG) を返します。  
  4. }  
  5. RFuture <長い>   ttlRemainingFuture = tryLockInnerAsync (commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  6.   ttlRemainingFuture.onComplete((ttlRemaining, e) - > {  
  7. e != null の場合 
  8. 戻る;  
  9. }  
  10. 残り時間== null の場合 
  11. スケジュール有効期限更新(スレッドID)  
  12. }  
  13. });  
  14. ttlRemainingFuture を返します。  
  15. }  
  16. < T > RFuture < T > tryLockInnerAsync(long リースタイム、TimeUnit ユニット、long スレッド ID、RedisStrictCommand < T >コマンド) {  
  17. 内部ロックリース時間= unit .toMillis(リース時間);
  18. evalWriteAsync(getName(), LongCodec.INSTANCE, コマンド, を返します。  
  19. 「(redis.call('exists', KEYS[1]) == 0) の場合」 +  
  20. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  
  21. "redis.call('pexpire', KEYS[1], ARGV[1]); " +  
  22. "nil を返す; " +  
  23. 「終了;」+  
  24. 「(redis.call('hexists', KEYS[1], ARGV[2]) == 1) の場合」 +  
  25. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  
  26. "redis.call('pexpire', KEYS[1], ARGV[1]); " +  
  27. "nil を返す; " +  
  28. 「終了;」+  
  29. "redis.call('pttl', KEYS[1]); を返します",  
  30. Collections.singletonList(getName())、internalLockLeaseTime、getLockName(threadId));  
  31. }

ロックの削除

  1. パブリック RFuture < Void > unlockAsync(long threadId) {  
  2. RPromise <無効>  結果=新しいRedissonPromise < Void > ();  
  3. RFuture <ブール値>  将来= unlockInnerAsync (スレッドID);  
  4. future.onComplete((opStatus, e) - > {  
  5. 有効期限の更新をキャンセルします(スレッドID);  
  6. e != null の場合
  7.   結果.tryFailure(e);  
  8. 戻る;  
  9. }  
  10. opStatusが null の場合
  11.   IllegalMonitorStateException原因= new IllegalMonitorStateException("ロックを解除しようとしましたが、ノード ID によって現在のスレッドによってロックされていません: "  
  12. + id + " スレッドID: " + threadId);  
  13. 結果.tryFailure(原因);  
  14. 戻る;
  15.   }  
  16. 結果.trySuccess(null);  
  17. });  
  18. 結果を返します。  
  19. }  
  20. 保護された RFuture <ブール値> unlockInnerAsync(long threadId) {  
  21. evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, を返します。  
  22. 「(redis.call('hexists', KEYS[1], ARGV[3]) == 0) の場合」 +  
  23. "nil を返します。" +  
  24. 「終了;」+  
  25. "ローカルカウンタ= redis .call('hincrby', KEYS[1], ARGV[3], -1); " +  
  26. 「もし(カウンタ 0)ならば」+  
  27. "redis.call('pexpire', KEYS[1], ARGV[2]); " +  
  28. "0を返す; " +  
  29. 「その他」+  
  30. "redis.call('del', KEYS[1]); " +  
  31. "redis.call('publish', KEYS[2], ARGV[1]); " +  
  32. "1を返す; " +  
  33. 「終了;」+  
  34. "nil を返す;",  
  35. Arrays.asList(getName(), getChannelName())、LockPubSub.UNLOCK_MESSAGE、internalLockLeaseTime、getLockName(threadId));  
  36. }

要約する

同時実行性の問題を解決するために Redis を分散ロックとして使用するには、まだいくつかの困難があり、注意が必要な点が多数あります。システムの規模を正しく評価し、特定の技術を使用するためだけにシステムを使用するべきではありません。同時実行の問題を完全に解決するには、データベース レベルで作業する必要があります。

<<:  Kubernetes の他に、重要なコンテナ オーケストレーション ツールは何ですか?

>>:  クラウドコンピューティングの8つの主な特徴

推薦する

中国オーディオブック著作権侵害防止連盟が設立され、初の権利保護訴訟を開始

中国オーディオブック著作権侵害防止連盟の設立式新浪科技は4月23日、電子書籍やモバイル読書の発展に伴...

サーバーレスはクラウドコンピューティングの未来でしょうか?

ニューヨーク・タイムズの最高技術責任者ニック・ロックウェル氏は、クラウド・コンピューティングが企業に...

企業の長期的な利用には、BaiduプロモーションとSEO最適化のどちらが適しているかを探る

現在、多くの企業は、企業が多くの顧客を獲得できる百度入札プロモーションに加えて、現在非常に人気があり...

クラウド コンピューティングのバックアップはデータ センターのバックアップとどう違うのでしょうか?

バックアップは企業にとって良い戦略です。企業は、自然災害や人為的ミスが発生した場合でも業務を正常に継...

モバイルインターネットマーケティングのトレンド

「これまで、モバイル インターネット マーケティングといえば、まず「SMS マーケティング」を思い浮...

メールマーケティングで開封率とコンバージョン率を高める8つのヒント

これらのシンプルでありながらあまり知られていない電子メール マーケティング戦略は、開封率とコンバージ...

競合他社を分析してウェブサイトのランキングを向上させる方法 - A5 Webmaster Network

SEO で成功したいなら、競合相手の分析方法を学ばなければなりません。自分自身と敵を知ることで、あら...

オンライン マーケティングのための最も正確な 5 つのトラフィック チャネルの分析!

トラフィックは、現在あらゆる分野の人が注目しているホットな話題です。トラフィックはデータと人気をもた...

今こそクラウドコンピューティングのデータの霧を晴らす時です

今日の企業は、クラウド アプリケーションやモバイル デバイスなど、多くのテクノロジを導入しており、従...

タレントネットワークのユーザー価値

最近、地域の人材ネットワークを運営している友人から、地元のウェブサイトや企業が広告主を探しているとい...

外部リンクを送信するにはどうすればいいですか?外部リンク大量送信ツールとは何ですか?

月収10万元の起業の夢を実現するミニプログラム起業支援プラン外部リンクは一般的に各プラットフォーム上...

認定資格やリモートワークがクラ​​ウドコンピューティングの給与に与える影響

COVID-19 パンデミックにより、クラウド コンピューティング関連の職種の需要が急増しており、エ...

ブランドが不在でPinterestの勢いが失われる

北京時間7月23日、海外メディアの報道によると、Pinterestはかつてソーシャルメディアの寵児だ...

外部リンクの交換と作成のいくつかの方法をまとめます

外部リンクは、Web サイトを最適化したり宣伝したりするための最も重要な方法の 1 つです。これは多...

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

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