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つの主な特徴

推薦する

finalhosting: 高性能オランダ VPS、6 ユーロ/2gDDR4/50gNVMe/1 コア i9-9900k/500M 無制限トラフィック/超高防御

オランダのfinalhostingデータセンターのKVM仮想VPSはハイエンドです。非常に高いメイン...

Niuche.com: 自動車分野では、Dianping、Zhihu、それとも Douban を選ぶべきでしょうか?

「何をしているんですか?自動車業界のDianpingですか?Zhihuですか?それともDoubanで...

タオバオをうまく運営するには?私の個人的な意見

「失敗の理由は何千もあるが、成功の理由は似ている」ということわざがあります。したがって、成功事例を分...

顧客獲得期間:チャネル管理をうまく行うには?

これまで、ユーザーライフサイクルの5つの主要な段階(顧客獲得、成長、成熟、衰退、離脱)と、その5つの...

スズメのようなマイクロ分散アーキテクチャを設計するにはどうすればよいでしょうか?

序文(本来の意図)このシステムを設計する本来の目的は、ビジネス (またはマシン クラスター) のスト...

名誉毀損と騒乱の疑いのあるネット有名人「秦火火」の裁判が4月11日に始まる

中国新聞社、4月8日。北京市朝陽区人民法院の公式微博によると、北京市朝陽区人民法院は、2014年4月...

[ケーススタディ] ニコンがソーシャルメディアを通じて自社ブランドを宣伝する方法

[ケーススタディ] ニコンがソーシャルメディアを通じて自社ブランドを宣伝する方法ワーナーミュージック...

#五一# メガレイヤー: 199 元/月、香港 CN2/米国 CN2、独立サーバー、e3-1230/8g メモリ/240gSSD または 1T HDD/30M 帯域幅無制限/3IPv4

megalayerはメーデーゴールデンウィーク特別プロモーションを開始しました:月額わずか199元、...

現状から判断すると!今日、SEO はどのような点に重点を置くべきでしょうか?

ウェブサイトの SEO に関しては、Baidu がアルゴリズムの更新、最適化、アップグレードを続けて...

国内Serverless Summit新記録樹立!第2回Techo TVP開発者サミットが北京で開催

サーバーレスは、将来のインフラストラクチャと運用に影響を与える上位 10 の技術トレンドの 1 つで...

ユーザーの検索サイクルを分析して検索エンジンマーケティングの効率を向上

ユーザーの検索領域は非常に広く、さまざまな生活情報がユーザー検索の主な行き先となっています。ユーザー...

ドギュンはどうですか? Dogyun 香港 MG データセンター BGP ライン クラウド サーバー評価

Dogyunは最近、香港のMGデータセンターのクラウドサーバーにアクセスしました。このサーバーは国際...

キーワードランキングの変動を解決する方法

ウェブマスターにとって最大の頭痛の種は、自分のウェブサイトのキーワードランキングが変動することです。...

2host-XEN PV VPS/サンノゼデータセンター/Gポート/無制限トラフィック

2009 年に設立された 2host は、現在 3,700 を超える顧客にホスティング サービスを提...

SFエクスプレスの生鮮食品eコマースの台頭から得た啓蒙:アルコール飲料はO2Oモデルに適している

双十一から20日近く経ちました。350億の取引高は、天猫にさらなるハイライトをもたらしたわけではなく...