7つのオプション! Redis 分散ロックの正しい使用法について議論する

7つのオプション! Redis 分散ロックの正しい使用法について議論する

[[385757]]

序文

日常の開発では、フラッシュセールの注文や紅包の受け取りなどのビジネスシナリオで分散ロックが必要になります。 Redis は分散ロックとして使用するのに非常に適しています。この記事では、Redis 分散ロックの正しい使用方法について 7 つのソリューションに分けて説明します。何か間違っている点がありましたら、ご指摘ください。一緒に学び、成長していきましょう。

公式アカウント:「カタツムリを拾う少年」

  • 分散ロックとは何ですか?
  • 解決策1: SETNX + EXPIRE
  • 解決策2: SETNX + 値は (システム時間 + 有効期限)
  • 解決策3: Luaスクリプトを使用する(SETNX + EXPIRE命令を含む)
  • 解決策4: SET拡張コマンド(SET EX PX NX)
  • 解決策5: SET EX PX NX + 一意のランダム値を検証し、ロックを解除する
  • ソリューション 6: オープンソースフレームワーク ~ Redisson
  • ソリューション7: 複数のマシンに実装された分散ロックRedlock

分散ロックとは何ですか?

分散ロックは、実際には、分散システム内のさまざまなプロセスが共有リソースにアクセスできるように制御するロックの実装です。重要なリソースが異なるシステム間または同じシステムの異なるホスト間で共有される場合、相互の干渉を防ぎ一貫性を確保するために、相互排他制御が必要になることがよくあります。

まず、信頼性の高い分散ロックに必要な機能について見てみましょう。

  • 「相互排他」: 一度にロックを保持できるのは 1 つのクライアントのみです。
  • 「ロック タイムアウトの解除」: ロックがタイムアウトの間保持されている場合、不要なリソースの浪費やデッドロックを防ぐために、ロックを解除できます。
  • 「再入可能」: スレッドがロックを取得すると、再度ロックを要求できます。
  • 「高パフォーマンスと高可用性」: ロックとロック解除のオーバーヘッドを可能な限り低く抑えると同時に、分散ロックの障害を回避するために高可用性を確保する必要があります。
  • 「セキュリティ」: ロックはそれを保持しているクライアントのみが削除でき、他のクライアントは削除できません。

Redis 分散ロックソリューション 1: SETNX + EXPIRE

Redis 分散ロックに関して言えば、多くの人はすぐに setnx+ expire コマンドを思い浮かべるでしょう。つまり、まず setnx を使用してロックを取得します。ロックが取得された場合は、expire を使用してロックの有効期限を設定し、ロックの解除を忘れないようにします。

SETNX は SET IF NOT EXISTS の略です。毎日のコマンドの形式は SETNX キー値です。キーが存在しない場合は、SETNX は正常に 1 を返します。キーが既に存在する場合は、0 を返します。

電子商取引ウェブサイト上の製品がフラッシュセールを行っていると仮定します。キーは key_resource_id に設定でき、値は任意の値に設定できます。疑似コードは次のとおりです。

  1. if (jedis.setnx(key_resource_id,lock_value) == 1) { //ロック
  2. 有効期限が切れます(キーリソースID、100); //有効期限を設定する
  3. 試す {
  4. 何かをする //ビジネスリクエスト
  5. }キャッチ(){
  6. }
  7. ついに {
  8. jedis.del(キーリソースID); //ロックを解除する
  9. }
  10. }

ただし、このソリューションでは、setnx コマンドと expire コマンドが分離されており、アトミック操作ではありません。 setnx を実行してロックを実行した後、有効期限を設定するために expire を実行しようとしたときにプロセスがクラッシュしたり、メンテナンスのために再起動する必要が生じたりした場合、ロックは「不滅」となり、「他のスレッドはロックを取得できなくなります」。

Redis 分散ロック ソリューション 2: SETNX + 値は (システム時間 + 有効期限)

最初の解決策「異常なロックを解除できないシナリオ」を解決するために、一部の友人は、有効期限を setnx の値に入れることができると考えています。ロックに失敗した場合は、値を取り出して再度確認してください。ロックコードは次のとおりです。

  1. 長い有効期限 = System.currentTimeMillis() + expireTime; //システム時間 + 有効期限の設定
  2. 文字列の有効期限Str = String.valueOf(有効期限);
  3.  
  4. // 現在のロックが存在しない場合は、ロック成功を返します
  5. jedis.setnx(key_resource_id, expiresStr) == 1 の場合 {
  6. 戻る 真実;
  7. }
  8. // ロックがすでに存在する場合は、ロックの有効期限を取得します
  9. 文字列 currentValueStr = jedis.get(key_resource_id);
  10.  
  11. // 取得した有効期限が現在のシステム時間より短い場合は、有効期限が切れていることを意味します
  12. currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis() の場合 {
  13.  
  14. // ロックの有効期限が切れました。前回のロックの有効期限を取得し、現在のロックの有効期限を設定します(Redis の getSet コマンドがわからない場合は、公式 Web サイトにアクセスして確認してください)
  15. 文字列 oldValueStr = jedis.getSet(key_resource_id, expiresStr);
  16.      
  17. oldValueStr != null && oldValueStr.equals(currentValueStr) の場合 {
  18. // マルチスレッド同時実行の状況を考慮すると、設定値が現在の値と同じ場合、1つのスレッドのみがロックできます。
  19. 戻る 真実;
  20. }
  21. }
  22.          
  23. // それ以外の場合、ロックは失敗します。
  24. 戻る 間違い;
  25. }

このソリューションの利点は、expire で有効期限を個別に設定する操作を巧みに削除し、「有効期限」を setnx の値に入れることです。ソリューション 1 で例外が発生したときにロックを解除できないという問題を解決しました。ただし、このソリューションには他にも欠点があります。

  • 有効期限はクライアント自体によって生成されます (System.currentTimeMillis() は現在のシステム時刻です)。分散環境では、各クライアントの時刻を同期する必要があります。
  • ロックの有効期限が切れると、複数のクライアントが同時にロックを要求し、jedis.getSet() を実行します。最終的に、ロックを正常にロックできるのは 1 つのクライアントだけですが、クライアント ロックの有効期限は他のクライアントによって上書きされる可能性があります。
  • ロックは所有者の一意の識別子を保存しないため、他のクライアントによって解除/ロック解除される可能性があります。

Redis 分散ロック ソリューション 3: Lua スクリプトの使用 (SETNX + EXPIRE 命令を含む)

実際、Lua スクリプトを使用してアトミック性を確保することもできます (setnx および expire 命令を含む)。 Lua スクリプトは次のとおりです。

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

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

  1. 文字列 lua_scripts = "redis.call('setnx',KEYS[1],ARGV[1]) == 1 の場合" +
  2. " redis.call('expire',KEYS[1],ARGV[2]) 1を返す、そうでなければ0を返す 終了" ;
  3. オブジェクト結果 = jedis.eval(lua_scripts、Collections.singletonList(key_resource_id)、Collections.singletonList( values ​​));
  4. //成功したかどうか判断する
  5. 結果を返します。equals(1L);

この計画と計画 2 を比較すると、どちらが良いと思いますか?

Redis 分散ロック ソリューション 4: SET 拡張コマンド (SET EX PX NX)

Lua スクリプトを使用して 2 つの命令 SETNX + EXPIRE のアトミック性を保証することに加えて、Redis の SET 命令を巧みに使用してパラメータを拡張することもできます。 (SET キー値 [EX 秒] [PX ミリ秒] [NX|XX]) これもアトミックです。

キー値の設定[EX 秒][PX ミリ秒][NX|XX]

  • NX: キーが存在しない場合にのみセットが成功できることを示します。これにより、最初のクライアント要求のみがロックを取得でき、他のクライアント要求はロックが解放された後にのみロックを取得できるようになります。
  • EX 秒: キーの有効期限を設定します。時間単位は秒です。
  • PXミリ秒: キーの有効期限をミリ秒単位で設定します
  • XX: キーが存在する場合にのみ値を設定する

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

  1. if (jedis.set ( key_resource_id, lock_value, "NX" , "EX" , 100s) == 1) { // ロック
  2. 試す {
  3. 何かをする //ビジネス処理
  4. }キャッチ(){
  5. }
  6. ついに {
  7. jedis.del(キーリソースID); //ロックを解除する
  8. }
  9. }

ただし、この解決策にはまだ問題がある可能性があります。

  • 質問1:「ロックの期限が切れて解除されましたが、業務が完了しませんでした。」スレッド a がロックを正常に取得し、クリティカル セクション内のコードを実行していると仮定します。しかし、100 秒経っても実行が完了しません。ただし、この時点でロックの有効期限が切れており、スレッド B が再度ロックを要求します。明らかに、スレッド b はロックを正常に取得し、クリティカル セクション コードの実行を開始できます。すると、クリティカル セクション内のビジネス コードが厳密にシリアルに実行されないという問題が発生します。
  • 質問 2: 「ロックは別のスレッドによって誤って削除されました。」スレッド a が実行後にロックを解除すると仮定します。ただし、現在のロックがスレッド b によって保持されている可能性があることはわかりません (スレッド a がロックを解放したときに有効期限が到来し、スレッド b が入り込んでロックを占有する可能性があります)。その後、スレッド a はスレッド b のロックを解除しますが、スレッド b のクリティカル セクションのビジネス コードはまだ実行されていない可能性があります。

解決策5: SET EX PX NX + 一意のランダム値を確認してから削除する

ロックは他のスレッドによって誤って削除される可能性があるため、値に現在のスレッドに固有の乱数を設定し、削除時にそれをチェックします。疑似コードは次のとおりです。

  1. if (jedis.set ( key_resource_id, uni_request_id, "NX" , "EX" , 100s) == 1) { //ロック
  2. 試す {
  3. 何かをする //ビジネス処理
  4. }キャッチ(){
  5. }
  6. ついに {
  7. //現在のスレッドによってロックが追加されたかどうかを判断し、追加された場合は解放します
  8. uni_request_id.equals(jedis.get(key_resource_id)) の場合 {
  9. jedis.del(ロックキー); //ロックを解除する
  10. }
  11. }
  12. }

ここで、「現在のスレッドによってロックが追加されたかどうかを判断する」ことと「ロックを解放する」ことはアトミック操作ではありません。ロックを解除するために jedis.del() が呼び出されると、ロックは現在のクライアントに属しなくなり、他のクライアントによって追加されたロックが解除されます。

より厳密にするために、代わりに Lua スクリプトが一般的に使用されます。 lua スクリプトは次のとおりです。

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

Redis 分散ロック ソリューション 6: Redisson フレームワーク

ソリューション 5 では、「ロックの有効期限が切れて解除され、業務が完了しない」という問題が依然として残る可能性があります。ロックの有効期限をもう少し長く設定すれば十分だと考える友人もいます。実際に、ロックを取得したスレッドに対して時間指定のデーモン スレッドを起動し、一定の間隔でロックがまだ存在するかどうかを確認できるかどうかを想像してみましょう。そうなる場合、ロックの有効期限が延長され、期限切れによりロックが早期に解除されるのを防ぎます。

現在のオープンソースフレームワークRedissonはこの問題を解決します。 Redisson の基本的な概略を見てみましょう。

スレッドが正常にロックされるとすぐに、ウォッチドッグが開始されます。 10 秒ごとにチェックするバックグラウンド スレッドです。スレッド 1 がまだロックを保持している場合、ロック キーの有効期間は継続的に延長されます。そのため、Redisson は Redisson を使用して、「ロックの有効期限が切れて解除されても業務が完了しない」という問題を解決します。

Redis 分散ロックソリューション 7: 複数のマシンで実装された分散ロック Redlock+Redisson

前の 6 つのソリューションは、スタンドアロン バージョンに関する議論のみに基づいており、まだ完璧ではありません。実際、Redis は一般的にクラスターでデプロイされます。

スレッド 1 が Redis マスター ノードのロックを取得したが、ロックされたキーがスレーブ ノードに同期されていない場合。ちょうどこのとき、マスターノードに障害が発生し、スレーブノードがマスターノードにアップグレードされます。スレッド 2 は同じキーでロックを取得できますが、スレッド 1 がすでにロックを取得しているため、ロックのセキュリティは失われます。

この問題を解決するために、Redis の作者 antirez は、高度な分散ロック アルゴリズムである Redlock を提案しました。 Redlock の中心的なアイデアは次のとおりです。

複数の Redis マスターをデプロイして、同時に障害が発生しないようにします。これらのマスターノードは互いに完全に独立しており、マスターノード間でデータの同期は行われません。同時に、これらの複数のマスター インスタンスでロックを取得および解放するには、単一の Redis インスタンスの場合と同じ方法が使用されるようにする必要があります。

現在 5 つの Redis マスター ノードがあり、これらの Redis インスタンスが 5 台のサーバー上で実行されていると想定します。

RedLock の実装手順は次のとおりです。

1. 現在の時刻をミリ秒単位で取得します。

2. 5 つのマスターノードに順番にロックを要求します。クライアントはネットワーク接続と応答のタイムアウトを設定します。タイムアウトはロックの有効期限よりも短くする必要があります。 (ロックが 10 秒後に自動的に期限切れになると仮定すると、タイムアウトは通常 5 ~ 50 ミリ秒の間になります。タイムアウトが 50 ミリ秒であると仮定します)。タイムアウトが発生した場合は、マスターノードをスキップし、できるだけ早く次のマスターノードを試してください。

3. クライアントは、現在の時刻からロックの取得が開始された時刻 (つまり、手順 1 で記録された時刻) を引いて、ロックの取得に使用された時刻を取得します。ロックが正常に取得されるのは、Redis マスター ノードの半分以上 (N/2+1、ここでは 5/2+1=3 ノード) がロックを取得し、使用された時間がロックの有効期限よりも短い場合のみです。 (上図のように、10秒>30ms+40ms+50ms+4m0s+50ms)

ロックが取得されると、キーの実際の有効期間が変わるため、ロックの取得に使用された時間を減算する必要があります。

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

簡略化された手順は次のとおりです。

  • 5つのマスターノードから順番にロックを要求する
  • 設定されたタイムアウトに基づいてマスターノードをスキップするかどうかを決定します。
  • 3 つ以上のノードが正常にロックされ、使用時間がロックの有効期間未満の場合、ロックは成功したと見なすことができます。
  • ロックの取得に失敗した場合は、ロックを解除してください。

Redisson はロックの redLock バージョンを実装します。興味があればぜひ行って学んでみてくださいね〜

参考文献と謝辞

Redisシリーズ: 分散ロック[1]

Redis分散ロックソリューションの簡単な分析[2]

Redis分散ロックの詳細な説明[3]

Redlock: Redis分散ロックの最も強力な実装

この記事はWeChatの公開アカウント「カタツムリを拾う少年」から転載したもので、著者はカタツムリを拾う少年です。この記事を転載する場合は、カタツムリを採る少年の公式アカウントまでご連絡ください。

<<:  分散トランザクション2PCおよび3PCモデルを徹底的に習得する

>>:  VMware は仮想化を加速するために SmartNIC をサポートしています

推薦する

安定した効率的なフレンドリーリンクを作成するための4つのステップ

2012年の4分の1が過ぎました。この間、SEOは高品質の外部リンクを最も重視してきました。品質は量...

過去を振り返り、未来に目を向けると、インターネットマーケティングとは何でしょうか?

「インターネット マーケティング」という用語には、実際には多くのことが含まれます。著者は学者ではない...

加速クラウド:四川徳陽高防御、825元/16コア/16gメモリ/200g SSD/50M帯域幅/100G防御(CC攻撃を無視)

加速クラウド(中華人民共和国付加価値通信事業許可証 B1-5344)は、四川省徳陽電信のコンピュータ...

百度の新戦略に対応する意外なウェブサイトデザイン

百度の最近の戦略更新により、インターネット上で「コンテンツ構築に重点を置く」という声が高まっています...

細部に注意を払うことで、ウェブサイトは優れた「ユーザーエクスペリエンス」を実現します。

ユーザー エクスペリエンスは、間違いなく昨今のホットな言葉です。最近の A5 の記事を例に挙げると、...

SEO ケーススタディ: Baidu スナップショットを操作して希望どおりに変更する

fgyb 氏はここ数日の観察に基づいて、興味深い現象を発見しました。それは、Baidu のスナップシ...

Bitronic - 512M メモリ (xen)/10GSSD/512G 月間トラフィック/5.09 USD/月

2007 年に設立されたと主張する Bitronictech は、ドメイン名、SSL 証明書、仮想ホ...

#黒5# anynode: ラスベガスの VPS、年間 8 ドルから、KVM/512M メモリ/1 コア/10gSSD/1T トラフィック

anynode、私はすでに知っていますが、ラスベガスのデータセンターの VPS ではブラックフライデ...

Baidu のこの激動の時代において、SEO はどのようにしてキーワードランキングを向上させることができるのでしょうか?

2012 年後半は、私たち SEO にとって激動の時期でした。Baidu が 6 月 28 日にアル...

Nagarro が数兆ドルのデジタル資産を効率的に管理するために Amazon Web Services を優先クラウド プロバイダーとして選択

Amazon Web Services は、世界的な IT サービスおよびコンサルティング企業である...

Guoheの実践: アプリケーションのライフサイクルに応じて異なる広告モデルを選択する

国内のモバイルインターネットは急速に発展しており、企業の具体的な実践事例を研究することは、この分野に...

ion: Krypt のクラウド サーバー ブランド、50% オフ、cn2 gt ネットワーク、Alipay が利用可能

Krypt 傘下のクラウド サーバー (VPS) ブランドである ion は現在、CN2 GT ネッ...

身近な事例からメールマーケティングのポイントを簡単に分析

プロモーションマーケティングといえば、誰もがSEO、Weiboマーケティング、QQグループマーケティ...

デル、仮想化技術を採用した新しいサーバーストレージを発表

デルは新しいサーバーおよびストレージ製品を発売すると発表した。デル製品と競合他社製品の違いは、競合他...