Zookeeper が分散ロックとして誕生したのはなぜですか?

Zookeeper が分散ロックとして誕生したのはなぜですか?

分散ロックとは何ですか?分散ロックは、分散システム間の共有リソースへの同期アクセスを制御する方法です。分散システムでは、多くの場合、それらの動作を調整する必要があります。

[[317600]]

画像はPexelsより

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

分散ロックを使用する理由は何ですか?

同時実行性の高い状況で、メソッドまたはプロパティが同じスレッドによってのみ同時に実行されるようにします。

モノリシック アプリケーションの従来の単一マシン展開の場合、Java 並行処理関連の API (ReentrantLock や Synchronized など) を使用して相互排他制御を行うことができます。単一マシン環境では、Java は並行処理関連の API を多数提供します。

しかし、ビジネス ニーズが進化するにつれて、元の単一マシン展開システムは分散クラスター システムへと進化します。分散システムはマルチスレッド、マルチプロセスであり、異なるマシンに分散されているため、元の単一マシン展開での同時実行制御ロック戦略は無効になります。シンプルな Java API では分散ロック機能を提供できません。

この問題を解決するには、共有リソースへのアクセスを制御するための JVM 間の排他制御メカニズムが必要です。これは分散ロックが解決する必要がある問題です。

たとえば、マシン A とマシン B はクラスターです。マシン A と B 上のプログラムは同じであり、高い可用性を備えています。

マシン A と B には、毎晩午前 2 時に実行する必要があるスケジュールされたタスクがありますが、このスケジュールされたタスクは 1 回しか実行できません。それ以外の場合はエラーが報告されます。

A と B が実行中の場合、ロックを取得する必要があります。ロックを取得した人は誰でも実行でき、ロックを取得できなかった人は実行する必要はありません。

ロック処理

ロックは次のように処理されます。

  • 単一のアプリケーションでロックを使用する: (単一プロセス、マルチスレッド) 同期します。
  • 分散ロックは、分散システム間でリソースへのアクセスを同期する方法です。
  • 分散ロックは、分散システム間で共有リソースへのアクセスを同期する方法です。

分散ロックの実装

分散ロックは次のように実装されます。

  • データに基づく楽観的ロックに基づく分散ロックの実装
  • Zookeeper 一時ノードに基づく分散ロック
  • Redis ベースの分散ロック

Redis 分散ロック

ロックを取得

set コマンドには、コマンドの動作を変更するために使用できるオプションが多数あります。以下は、set コマンドで使用できるオプションの基本構文です。

  1. redis 127.0.0.1:6379>設定 キー値 [EX 秒] [PX ミリ秒] [NX|XX]
  2.  
  3. - EX秒 指定された有効期限を設定します(秒単位)
  4. - PXミリ秒は指定された有効期限を設定します(ミリ秒単位)
  5. - NX: キーが存在しない場合にのみ設定する
  6. - XX: キーが既に存在する場合にのみ設定

方法1: 紹介

  1. プライベート静的最終文字列 LOCK_SUCCESS = "OK" ;
  2. プライベート静的最終文字列 SET_IF_NOT_EXIST = "NX" ;
  3. プライベート静的最終文字列 SET_WITH_EXPIRE_TIME = "PX" ;
  4.  
  5. パブリック静的ブール値 getLock(JedisCluster jedisCluster、文字列 lockKey、文字列 requestId、 int expireTime) {
  6. // NX: 相互排他性を確保する
  7. 文字列結果 = jedisCluster。設定(lockKey、requestId、SET_IF_NOT_EXIST、SET_WITH_EXPIRE_TIME、expireTime);
  8. if (LOCK_SUCCESS.equals(結果)) {
  9. 戻る 真実;
  10. }
  11. 戻る 間違い;
  12. }

方法2:

  1. 公共 静的ブール値 getLock(String lockKey,String requestId, int expireTime) {
  2. 長い結果 = jedis.setnx(lockKey, requestId);
  3. 結果 == 1 の場合
  4. jedis.expire(ロックキー、有効期限);
  5. 戻る 真実;
  6. }
  7. 戻る 間違い;
  8. }

注: 方法 2 の setnx と expire は 2 つの操作であり、アトミック操作ではないため、方法 1 が推奨されます。 setnx に問題がある場合はデッドロックが発生するため、方法 1 をお勧めします。

ロックを解除

方法1: delコマンドの実装

  1. 公共 静的void releaseLock(文字列lockKey、文字列requestId) {
  2. リクエストIDがjedis.get(lockKey)と等しい場合
  3. jedis.del(ロックキー);
  4. }
  5. }

方法2: Redis+Lua スクリプト実装 (推奨)

  1. 公共 静的ブール型 releaseLock(String lockKey, String requestId) {
  2. 文字列スクリプト = "if redis.call( 'get' , KEYS[1]) == ARGV[1] then  戻る 
  3. redis.call( 'del' , KEYS[1]) else   0 を返す終了";
  4. オブジェクト結果 = jedis.eval(script, Collections.singletonList(lockKey),
  5. Collections.singletonList(リクエストID));
  6. (結果が1Lに等しい場合){
  7. 戻る 真実;
  8. }
  9. 戻る 間違い;
  10. }

Zookeeper の分散ロック

Zookeeper 分散ロック実装の原則

ロックの原理を理解すると、Zookeeper が分散ロックの自然な胚であることがわかります。

まず、各 Zookeeper ノードは自然なシーケンサーです。

各ノードの下に子ノードを作成する場合、選択した作成タイプが順序付けされている限り (EPHEMERAL_SEQUENTIAL 一時順序付けまたは PERSISTENT_SEQUENTIAL 永続順序付け)、新しい子ノードの後に​​シーケンス番号が追加されます。

このシーケンス番号は、最後に生成されたシーケンス番号に 1 を加えたものです。たとえば、番号を発行するためのノード「/test/lock」を作成し、それを親ノードとして使用して、この親ノードの下に同じプレフィックスを持つ子ノードを作成します。

同じプレフィックスが「/test/lock/seq-」であると仮定し、子ノードを作成するときに、順序付き型を同時に指定します。

最初に作成された子ノードの場合、生成される子ノードは /test/lock/seq-0000000000、次のノードは /test/lock/seq-00000000001 などになります。

第二に、Zookeeper ノードの増分的な性質により、ノード番号が最も小さいノードがロックを取得するように規定できます。

Zookeeper 分散ロックでは、まず親ノード (できれば永続ノード (PERSISTENT タイプ)) を作成する必要があります。その後、ロックを取得しようとする各スレッドがこのノードの下に一時シーケンス ノードを作成します。シーケンス番号は増加する性質があるため、最も小さいシーケンス番号を持つものがロックを取得するように規定できます。

したがって、各スレッドがロックを占有しようとする前に、まずそのキュー番号が現時点で最小であるかどうかを判断します。そうであれば、ロックを取得します。

3 番目に、Zookeeper のノード監視メカニズムにより、ロック占有方法が整然として効率的であることが保証されます。

各スレッドはロックを取得する前に、まず番号を取得して独自の ZNode を作成します。同様に、ロックを解除するときは、番号を取得した Znode を削除する必要があります。

番号の取得に成功した後、最も小さい番号のノードでない場合は、通知を待つ状態になります。誰からの通知を待っていますか?他には何も必要ありません。前の Znode からの通知を待つだけです。

現在の Znode が削除されると、ロックを占有する順番が変わります。 1 番目が 2 番目に通知し、2 番目が 3 番目に通知し、荷物を渡すように順番に荷物を渡します。

Zookeeper のノード監視メカニズムは、このような荷物の受け渡しのような情報伝達を実現するのに非常に最適であると言えます。

具体的な方法は、通知を待機している各 Znode ノードが linsten をリッスンするか、その前にあるノードを監視するだけでよいというものです。

前のノードが削除されると、それが最小のシーケンス番号を持つノードであるかどうかが再度チェックされます。そうであれば、ロックを取得します。

Zookeeper のノード監視メカニズムがこれほど完璧なのはなぜでしょうか?

プロセスが最初から最後までつながっていて、後ろのプロセスが前のプロセスを監視している場合、途中で切断される心配はありませんか?たとえば、分散環境で、ネットワークの問題、サーバーの障害、またはその他の理由により、前のノードがプログラムによって正常に削除できない場合、後続のノードは永久に待機するのではないでしょうか。

実際、Zookeeper の内部メカニズムにより、後続のノードが削除を監視し、正常にロックを取得できるようになります。

Znode を作成するときは、永続的な Znode ではなく一時的な Znode を作成するようにしてください。

この Znode のクライアントが Zookeeper クラスター サーバーとの接続を失うと、この一時的な Znode は自動的に削除されます。その背後のノードも削除イベントを受信し、ロックを取得できます。

Zookeeper のノード監視メカニズムは完璧です。もう一つの理由があります。 Zookeeper のエンドツーエンドの接続では、バックがフロントをリッスンするため、群集効果を回避できます。

いわゆる群れ効果とは、各ノードがハングアップすると、すべてのノードが監視して応答するため、サーバーに多大な負荷がかかることです。したがって、一時的な連続ノードが存在します。ノードが切断されると、その背後のノードのみが応答します。

Zookeeper 分散ロックの実装例

Zookeeper は一時ノードを通じて分散ロックを実装します。

  1. 中断された例外 e) {
  2. e.printStackTrace();
  3. }
  4. システム。 out .println( "*********ビジネスメソッドが終了します************\n" );
  5.  
  6. }
  7.  
  8. // ここで@Testを使用するとエラーが発生します
  9. 公共 静的void main(String[] args) {
  10. // 再試行側の戦略を定義する 1000 待機時間 (ミリ秒) 10 再試行回数
  11. 再試行ポリシー ポリシー = 新しい ExponentialBackoffRetry(1000, 10);
  12.  
  13. // Zookeeperクライアントを定義する
  14. CuratorFramework クライアント = CuratorFrameworkFactory.builder()
  15. .connectString( "10.231.128.95:2181,10.231.128.96:2181,10.231.128.97:2181" )
  16. .retryPolicy(ポリシー)
  17. 。建てる();
  18. // クライアントを起動する
  19. クライアントを起動します。
  20.  
  21. // Zookeeper でロックを定義する
  22. 最終的な InterProcessMutex ロック = 新しい InterProcessMutex(client, "/mylock" );
  23.  
  24. //開始はスレッドです
  25. ( int i = 0; i <10; i++)の場合{
  26. 新しいスレッド(新しい実行可能() {
  27. @オーバーライド
  28. パブリックボイド実行(){
  29. 試す {
  30. // 要求されたロック
  31. ロックを取得します。
  32. 数値を印刷します。
  33. } キャッチ (例外 e) {
  34. e.printStackTrace();
  35. ついに
  36. // ロックを解除する
  37. 試す {
  38. ロックを解除します。
  39. } キャッチ (例外 e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. }
  44. })。始める();
  45. }
  46.  
  47. }
  48. }

データベース分散ロック

分散ロックの使用について議論する場合、データベース ベースのソリューションは十分に「高度」ではないと本能的に考え、まずそれを除外することがよくあります。

パフォーマンスの観点から見ると、データベース ベースのソリューションは確かに十分ではありません。全体的なパフォーマンスの比較は、キャッシュ > Zookeeper、etcd > データベースです。

また、データベースベースのソリューションには多くの問題があり、あまり信頼性が高くないと指摘する人もいました。データベース スキームは、頻繁な書き込み操作には適さない可能性があります。

データベース (MySQL) に基づくソリューションを見てみましょう。これは、一般的に次の 3 つのカテゴリに分けられます。

  • テーブルレコードに基づく
  • 楽観的ロック
  • 悲観的ロック

テーブルレコードに基づく

分散ロックを実装するには、ロック テーブルを直接作成し、テーブル内のデータを操作して実装するのが最も簡単な方法です。

ロックを取得したい場合はテーブルにレコードを追加し、ロックを解除したい場合はレコードを削除します。

よりわかりやすいデモンストレーションのために、まずは以下に示すようにデータベース テーブルを作成しましょう。

  1. 作成する テーブル`database_lock` (
  2. `id` BIGINT  ない  NULL AUTO_INCREMENT、
  3. `リソース` int  ない  NULLコメント「ロックされたリソース」
  4. `description` varchar (1024)ではない  NULL  デフォルト  コメント'説明'
  5. 主要な キー(`id`)、
  6. 個性的 キー`uiq_idx_resource` (`リソース`)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT= 'データベース分散ロックテーブル' ;

①鍵を手に入れる

データを挿入することができます:

  1. 入れる  INTO database_lock(リソース、説明) VALUES (1、 'lock' );

resource は database_lock テーブル内の唯一のインデックスであるため、データベースに送信された他の要求はエラーになり、正常に挿入されません。挿入できるリクエストは 1 つだけです。挿入が成功すると、ロックが取得されます。

②ロックを解除

  1. 入れる  INTO database_lock(リソース、説明) VALUES (1、 'lock' );

この実装は非常にシンプルですが、次の点に注意する必要があります。

① このタイプのロックには有効期限がありません。ロックを解除する操作が失敗すると、ロック レコードがデータベースに残り、他のスレッドはロックを取得できなくなります。この欠陥も簡単に解決できます。たとえば、定期的にクリーンアップするスケジュールされたタスクを作成できます。

②このロックの信頼性はデータベースに依存します。単一ポイントを回避し、信頼性をさらに向上させるために、バックアップ データベースを設定することをお勧めします。

③このロックは非ブロッキングです。データの挿入に失敗した場合はエラーが直接報告され、ロックを取得するために再度操作する必要があります。

ブロッキング メソッドが必要な場合は、for ループ、while ループなどを作成し、INSERT が成功するまで待ってから戻ることができます。

④このロックは再入不可能です。同じレコードがデータベース内に既に存在するため、ロックを解放する前に同じスレッドが再度ロックを取得することはできないからです。

再入可能ロックを実装するには、ロックのホスト情報やスレッド情報などのいくつかのフィールドをデータベースに追加できます。

その後、再度ロックを取得すると、最初にデータを照会できます。現在のホスト情報とスレッド情報が見つかった場合は、それにロックを直接割り当てることができます。

楽観的ロック

名前が示すように、システムはほとんどの場合データの更新によって競合が発生しないと想定し、データベース更新操作が送信されたときにのみデータの競合検出を実行します。テスト結果が予想されるデータと一致しない場合は、失敗メッセージが返されます。

最も楽観的なロックは、データ バージョン記録メカニズムに基づいて実装されます。データのバージョン番号は何ですか?

つまり、データにバージョン識別子を追加します。データベース テーブルに基づくバージョン ソリューションでは、通常、データベース テーブルに「バージョン」フィールドを追加することで実現されます。データの読み込み時にバージョン番号も一緒に読み出され、更新時にバージョン番号が 1 増加します。

更新プロセス中に、バージョン番号が比較されます。一貫性があり変更されていない場合、操作は正常に実行されます。バージョン番号が一致しない場合、更新は失敗します。

実際のプロジェクトにおけるデータベース オプティミスティック ロックの使用をよりよく理解するために、業界でよく言及されるインベントリの例を示します。

電子商取引プラットフォームには商品の在庫があり、ユーザーが購入すると在庫が操作されます(在庫が 1 減ると、商品が 1 つ売れたことを意味します)。

1 人のユーザーだけが操作を実行する場合、データベース自体がユーザーの操作の正確性を保証できますが、同時実行の状況では予期しない問題が発生します。

たとえば、2 人のユーザーが同時に製品を購入した場合、データベース レベルでの実際の操作は、在庫を 2 減らすことです。

ただし、同時実行性が高いため、最初のユーザーが購入を完了すると、現在の在庫を確認するためにデータが読み取られ、データが 1 減算されます。ただし、この操作は完全には実行されません。

2 番目のユーザーが同じ製品を購入するために来店します。このとき、照会されたインベントリは 1 減算されていないインベントリである可能性があり、その結果、ダーティ データ (スレッドセーフでない操作) が発生します。

データベースの楽観的ロックはスレッドの安全性も保証します。通常、これはコード レベルで行います。

  1. select goods_num from goods where goods_name = "小簿" ;
  2. 商品を更新します。goods_num = goods_num -1し、 goods_name = "Little Book"とします

上記のSQLはグループです。通常、最初に現在の goods_num が照会され、次に goods_num から 1 が減算されて在庫が変更されます。

同時実行の場合、このステートメントにより、元の在庫が 3 個だった製品を 2 人が購入した後に 2 個しか残っていないという状況が発生し、製品の過剰販売につながる可能性があります。では、データベースの楽観的ロックはどのように実装されるのでしょうか?

まず、バージョン番号として使用するバージョン フィールドを定義すると、各操作は次のようになります。

  1. goods_name = "Little Book"の場合、 goodsからgoods_num、versionを選択します
  2. 商品更新します。goods_num = goods_num -1に設定し、version = 照会されたバージョン値が増分されます。ただし、 goods_name は"小簿子"です。   version =照会されたバージョン;

実際、バージョン フィールドを使用する方法と同様に、更新タイムスタンプ (updated_at) の助けを借りて楽観的ロックを実現することもできます。

更新操作を実行する前に、現在の更新時刻が記録されます。更新を送信するときに、現在の更新時刻が更新の開始時に取得された更新タイムスタンプと等しいかどうかがチェックされます。

悲観的ロック

データベース テーブル内のレコードの追加と削除に加えて、データベースの組み込みロックを使用して分散ロックを実装することもできます。

クエリ ステートメントの後に FOR UPDATE を追加すると、データベースはクエリ プロセス中にデータベース テーブルに悲観的ロック (排他ロックとも呼ばれます) を追加します。レコードが悲観的にロックされると、他のスレッドはそれに悲観的ロックを追加できなくなります。

悲観的ロックは、楽観的ロックとは対照的に、ほとんどの場合にデータの更新が競合することを前提として、常に最悪のシナリオを想定します。

悲観的ロックを使用する場合は、ロック レベルに注意する必要があります。 MySQL InnoDB では、ロック時に主キー (またはインデックス) が明示的に指定されている場合にのみ行ロックが実行され (選択されたデータのみがロックされます)、それ以外の場合は MySQL によってテーブル ロックが実行されます (データ テーブル全体がロックされます)。

悲観的ロックを使用する場合、MySQL はデフォルトで自動コミット モードを使用するため、MySQL データベースの自動コミット プロパティをオフにする必要があります (以下の例を参照)。

つまり、更新操作を実行すると、MySQL は結果をすぐにコミットします。

  1. mysql> SET AUTOCOMMIT = 0;
  2. クエリは正常、影響を受けた行は 0(0.00 秒)

このように、FOR UPDATE を使用してロックを取得した後、対応するビジネス ロジックを実行し、実行後に COMMIT を使用してロックを解除することができます。

使用方法を詳しく説明するには、前の database_lock テーブルを使用するのもよいでしょう。ロックを取得して対応する操作を実行する必要があるスレッド A があるとします。

具体的な手順は以下のとおりです。

  1. ステップ1 - ロックを取得: SELECT * FROM database_lock WHERE id = 1 FOR  アップデート;。
  2. ステップ2 − ビジネスロジックを実行します。
  3. ステップ3 - ロックを解除します: COMMIT

著者: リン・ジン

自己紹介: 人生でジョークを言う人、現在は不動産会社でDevOPS関連の仕事をしています。以前は大手インターネット企業のシニア運用保守エンジニアで、Linuxの運用保守、Pythonの運用保守開発、Java開発、DevOPS共通開発コンポーネントなどに精通しています。 個人公開アカウント: stromling、私とのチャットを歓迎します!

<<:  マルチパス システム トレイ ツールを使用して仮想マシンを管理する方法を教えてください。

>>:  5Gがクラウドコンピューティングを変える2つの方法

推薦する

優れたeコマースウェブサイトの5つの特徴: 即時在庫追跡

ユーザー エクスペリエンス デザインの専門家である Gil Remy 氏は最近、UX Magazin...

lighthosting: マレーシアサーバー/X5560/16g/2x1T/100M無制限トラフィック

マレーシアの企業である Lighthosting は、2010 年頃からホスティング事業を開始しまし...

IOの基本原則を実装する方法

最近、IO の基本的な実装原則を学ぶために、JAVA NIO について読んでいます。 IO に関連す...

ウェブサイト間の競争における2つの方法の合理的な使用

今日では、ウェブサイトを運営するには競争がつきものです。小規模なウェブサイトには独自の競争相手がおり...

プロモーションにビデオを活用するには? Baiduビデオコレクションが大盛況!

月給5,000~50,000のこれらのプロジェクトはあなたの将来です動画プロモーションは多くの人にと...

公立病院検索における現在のレベル情報が医療 SEO に与える影響

今日、Souwai の Q&A チャンネルを閲覧していたところ、次のような質問を見つけました...

INXY のお知らせ: inxy.com が inxy.hosting に変更され、ウェブサイトが刷新され、VPS、CDN、専用サーバーが大幅値下げされました

HostCat は、安価な VPS、複数のデータセンターの独立したサーバー、超低価格の CDN を提...

Kubernetes で Apache Spark を実行する方法

[[416069]] [51CTO.com クイック翻訳] Empathyのプラットフォームエンジニ...

ラオ・モー:バイドゥのリンク取引取り締まり強化に関する雑感

1. ウェブサイトリンクの販売によって誰のチーズが動かされているのでしょうか?最近、百度がウェブサイ...

ガートナー:世界のパブリッククラウド支出は2022年に4,800億ドルを超える

ガートナーはレポートの中で、パブリッククラウド業界が今後数年間で大幅な成長を遂げると予測しました。 ...

Baidu の大規模アップデート後にウェブサイトを最適化する方法

皆さんご存知の通り、Baiduはしばらく前に大きなアップデートを行い、多くのウェブサイトのキーワード...

ウェブサイト運営の基本:信頼できるウェブサイトを作る方法

最近、デポン物流のウェブサイトに多くの「偽」ウェブサイトが出現し、公式サイトの業務を妨害しただけでな...

ウェブページのキーワードを変更するときに検索エンジンを馬鹿にしてはいけない

著者はかつて多くのウェブマスターと同じでした。当初、ウェブサイトのポジショニングが不正確だったため、...

Fastdomainの簡単な紹介

Fastdomain は 2005 年に設立され、ドメイン名と仮想ホスティング サービスを提供してい...

サイト最適化の最初のステップ、URL リンクが正しく設定されているかどうか確認してください。

月給5,000~50,000のこれらのプロジェクトはあなたの将来ですはじめに:ウェブサイトへのURL...