[[391029]]序文Java ではロックはよく知られており、よく使用されるのは synchronized と Lock です。 Java 並行プログラミングでは、複数のスレッドが同じ共有リソースまたは変数を競合することによって発生するデータの不整合の問題を解決するためにロックを使用します。ただし、JVM ロックは単一のアプリケーション サービスにのみ使用できます。当社のビジネスの開発ニーズに合わせて、単一マシンで展開されたシステムは、長い間、分散システムへと進化してきました。分散システムには複数のスレッドとプロセスがあり、異なるマシンに分散されているため、現時点では JVM ロックの同時実行制御は無効です。 JVM 間のロックを解決し、共有リソースへのアクセスを制御するために、分散ロックが生まれました。
分散ロックとは何ですか?分散ロックは、分散システム間で共有リソースへのアクセスを同期する方法です。分散システムでは、多くの場合、それらの動作を調整する必要があります。異なるシステムまたは同じシステムの異なるホストが 1 つのリソースまたはリソースのグループを共有する場合、これらのリソースにアクセスするときに、相互の干渉を防ぎ一貫性を確保するために相互排他制御が必要になることがよくあります。この場合、分散ロックが必要になります。 分散環境で JVM ロックが不可能なのはなぜですか?コードを見て、クラスター内で JVM ロックが信頼できない理由を確認しましょう。商品の急ぎ購入のシナリオをシミュレーションしてみましょう。サービス A には製品の購入に急いでいるユーザーが 10 人おり、サービス B には製品の購入に急いでいるユーザーが 10 人います。ユーザーの 1 人が商品を購入すると、他のユーザーはその商品を注文できなくなります。では、製品を受け取るのはサービス A でしょうか、それともサービス B でしょうか?見てみましょう。 いずれかのユーザーがアイテムを正常に購入すると、ステータスが 1 に変わります。
グラブサービス:- パブリックインターフェースGrabService {
-
- /**
- * 商品の奪取
- * @param 注文ID
- * @param ドライバーID
- * @戻る
- */
- パブリックResponseResult grabOrder( int orderId, int driverId);
- }
GrabJvmLockService実装:- @Service( "JvmLockService" ) は、
- パブリッククラスGrabJvmLockServiceImplはGrabServiceを実装します{
-
- オートワイヤード
- 注文サービス注文サービス;
-
- @オーバーライド
- パブリックレスポンス結果 grabOrder( int orderId, int driverId) {
- 文字列ロック = (orderId + "" );
-
- 同期化 (lock.intern()) {
- 試す {
- システム。 out .println( "ユーザー: " +driverId+ " 命令ロジックを実行" );
-
- ブール値 b = orderService.grab(orderId, driverId);
- もし(b) {
- システム。 out .println( "ユーザー: " +driverId+ " 注文が正常に完了しました" );
- }それ以外{
- システム。 out .println( "ユーザー: " +driverId+ "注文が失敗しました" );
- }
- ついに
-
- }
- }
- 戻る ヌル;
- }
- }
注文サービス:- パブリックインターフェースOrderService{
- パブリックブール値グラブ( int orderId, int driverId);
- }
注文サービスの実装:- @サービス
- パブリッククラス OrderServiceImpl は OrderService を実装します {
-
- オートワイヤード
- プライベート OrderMapper マッパー;
-
- パブリックブールグラブ( int orderId, int driverId) {
- 注文 順序= mapper.selectByPrimaryKey(orderId);
- 試す {
- スレッドをスリープ状態にします(1000);
- } キャッチ (InterruptedException e) {
- e.printStackTrace();
- }
- if( order .getStatus().intValue() == 0) {
- 注文.setStatus(1);
- mapper.updateByPrimaryKeySelective(順序);
-
- 戻る 真実;
- }
- 戻る 間違い;
-
- }
- }
ここでは、クラスター環境をシミュレートし、アクセス用に 8004 と 8005 の 2 つのポートを起動します。ここではテストにjmeterを使用します。 jmeter を知らない場合は、Tomcat のストレステストに関する以前の記事をお読みください: tomcat の最適化 プロジェクトの起動シーケンス: 最初に Server-eureka 登録センターを起動し、次にポート 8004 と 8005 を起動します。 テスト結果:
ここでは、ユーザーがサービス 8004 とサービス 8005 の両方でこの製品の注文を正常に行ったが、この製品を取得できるのは 1 人のユーザーだけであることがわかります。したがって、JVM ロックがクラスターまたは分散環境にある場合、共有変数のデータに同時にアクセスできるスレッドが 1 つだけであることを保証できず、分散環境およびクラスター環境の問題を解決できません。したがって、分散ロックが必要になります。 分散ロックを実装する3つの方法分散ロックを実装する方法は 3 つあります。 - データベースに基づく分散ロックの実装
- キャッシュに基づく分散ロックの実装 (Redis)
- Zookeeperに基づく分散ロックの実装今日は主にRedisに基づく分散ロックについてお話します
Reidsは3つの方法で分散ロックを実装する1. RedisのSETNXをベースにした分散ロックの実装 2. Redissonは分散ロックを実装する 3. redLockを使用して分散ロックを実装する ディレクトリ構造:
方法1: SETNXに基づく分散ロックの実装キーが存在しない場合にのみ、キーの値を値に設定します。指定されたキーがすでに存在する場合、SETNX は何も実行しません。 setnx: キーが存在する場合は何もしません。キーが存在しない場合は設定する ロック:- 注文IDとドライバーIDをNX PX 30000に設定
- 上記のコマンドが正常に実行されると、クライアントはロックを正常に取得し、共有リソースにアクセスできるようになります。上記のコマンドが失敗した場合、ロックの取得に失敗したことを意味します。
ロックを解除する: 重要なのは、自分が追加したロックかどうかを判断することです。 グラブサービス:- パブリックインターフェースGrabService {
-
- /**
- * 商品の奪取
- * @param 注文ID
- * @param ドライバーID
- * @戻る
- */
- パブリックResponseResult grabOrder( int orderId, int driverId);
- }
GrabRedisLockServiceImpl :- @Service( "RedisLockService" ) は、
- パブリッククラスGrabRedisLockServiceImplはGrabServiceを実装します{
-
- オートワイヤード
- 文字列Redisテンプレート stringRedisTemplate;
-
- オートワイヤード
- 注文サービス注文サービス;
-
- @オーバーライド
- パブリックレスポンス結果 grabOrder( int orderId , int driverId){
- //キーを生成する
- 文字列ロック = "order_" +(orderId+ "" );
- /*
- * ケース 1: 実行前にロックが解除されない場合、たとえば、業務ロジックが途中で実行されたり、運用保守によりサービスが再起動されたり、サーバーがハングアップして最終的に終了しなかったりする場合は、どうすればよいでしょうか。
- * タイムアウトを追加
- */
- // ブール型 lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+ "" );
- // if(!lockStatus) {
- //戻る ヌル;
- // }
-
- /*
- * ケース2:タイムアウト期間を超過した場合、システムはデータを追加できない可能性があるため、運用保守担当者がシステムを再起動します。
- */
- // ブール型 lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+ "" );
- // stringRedisTemplate.expire(lock.intern(), 30L, TimeUnit.SECONDS);
- // if(!lockStatus) {
- //戻る ヌル;
- // }
-
- /*
- * ケース 3: タイムアウトは 2 行のコードではなく 1 回追加する必要があります。
- *
- */
- ブール lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+ "" , 30L, TimeUnit.SECONDS);
- if(!lockStatus) {
- 戻る ヌル;
- }
-
- 試す {
- システム。 out .println( "ユーザー: " +driverId+ " 注文取得ロジックを実行します" );
-
- ブール値 b = orderService.grab(orderId, driverId);
- もし(b) {
- システム。 out .println( "ユーザー: " + driverId + " 注文が正常に取得されました" );
- }それ以外{
- システム。 out .println( "ユーザー: " +driverId+ " 注文を取得できませんでした" );
- }
-
- ついに
- /**
- ※この種類のロック解除により、他の人のロックが解除される可能性があります。
- */
- // 文字列RedisTemplate.delete ( lock.intern());
-
- /**
- * 次のコードは他の人のロックを解除しないようにします
- */
- if((driverId+ "" ).equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
- 文字列RedisTemplate.delete (lock.intern());
- }
- }
- 戻る ヌル;
- }
- }
ここで誰かが尋ねるかもしれません、私のビジネスの実行時間がロック解除時間を超えた場合はどうすればいいですか?デーモンスレッドを使用できます。現在のスレッドがまだロックを保持している限り、10 秒が経過すると、デーモン スレッドはロックが解放されるまでスレッドの有効期限を 30 秒自動的に延長します。更新されません。子スレッドが開始されます。元の時間は N で、N/3 ごとに N 秒間更新されます。 集中:- key は注文 ID など、ロックする対象です。
- driverId は当社の製品 ID であり、十分に長い期間にわたってすべてのクライアントからのすべてのロック取得要求の中で一意である必要があります。つまり、1 つの注文は 1 人のユーザーによって取得されます。
- NX は、orderId が存在しない場合にのみ SET が成功することを意味します。これにより、最初に要求したクライアントのみがロックを取得でき、ロックが解除されるまで他のクライアントはロックを取得できなくなります。
- PX 30000 は、このロックの自動有効期限が 30 秒であることを意味します。もちろん、30 秒は単なる例であり、クライアントは適切な有効期限を選択できます。
- このロックには有効期限を設定する必要があります。そうでない場合、クライアントがロックを正常に取得しても、クラッシュしたり、ネットワーク パーティションが原因で Redis ノードと通信できなくなったりすると、クライアントは常にロックを保持し、他のクライアントはロックを取得できなくなります。 Antirez はその後の分析でもこの点を強調し、この有効期限をロック有効時間と呼びました。ロックを取得したクライアントは、この時間内に共有リソースへのアクセスを完了する必要があります。
- この操作は分割できません。 >SETNX orderId driverId EXPIRE orderId 30 これらの 2 つのコマンドは、前のアルゴリズムの説明の SET コマンドと同じ実行効果がありますが、アトミックではありません。 SETNX を実行した後にクライアントがクラッシュした場合、EXPIRE を実行する機会がなくなり、ロックが永久に保持されることになります。デッドロックが発生します。
方法2: redissonに基づく分散ロックの実装フローチャート:
コード実装:- @Service( "RedisRedissonService をグラブする" )
- パブリッククラスGrabRedisRedissonServiceImplはGrabServiceを実装します{
-
- オートワイヤード
- RedissonClient;
-
- オートワイヤード
- 注文サービス注文サービス;
-
- @オーバーライド
- パブリックレスポンス結果 grabOrder( int orderId , int driverId){
- //キーを生成する
- 文字列ロック = "order_" +(orderId+ "" );
-
- RLock rlock = redissonClient.getLock(lock.intern());
-
-
- 試す {
- // このコードは、キーのタイムアウトをデフォルトで30秒に設定し、10秒後に遅延させます
- rlock.lock();
- システム。 out .println( "ユーザー: " +driverId+ " 注文取得ロジックを実行します" );
-
- ブール値 b = orderService.grab(orderId, driverId);
- もし(b) {
- システム。 out .println( "ユーザー: " + driverId + " 注文が正常に取得されました" );
- }それ以外{
- システム。 out .println( "ユーザー: " +driverId+ " 注文を取得できませんでした" );
- }
-
- ついに
- rlock.unlock();
- }
- 戻る ヌル;
- }
- }
集中: 1. Redis 障害の問題。 Redis に障害が発生すると、すべてのクライアントがロックを取得できなくなり、サービスが利用できなくなります。使いやすさを向上するため。 Redis のマスターとスレーブを設定します。マスターが利用できない場合、システムはスレーブに切り替わります。 Redis のマスター/スレーブ レプリケーションは非同期であるため、ロック セキュリティが失われる可能性があります。 - 1. クライアント 1 はマスターからロックを取得します。 2. マスターがダウンしており、ストレージ ロック キーがスレーブにまだ同期されていません。 3.スレーブがマスターにアップグレードされます。 4. クライアント 2 は、新しいマスターから同じリソースに対応するロックを取得します。
クライアント 1 とクライアント 2 は同時に同じリソースのロックを保持します。ロックのセキュリティが破られました。 2. 適切なロック有効時間はどれくらいですか?設定が短すぎると、クライアントが共有リソースへのアクセスを完了する前にロックが期限切れになり、保護が失われる可能性があります。設定が長すぎると、ロックを保持しているクライアントがロックの解放に失敗すると、他のすべてのクライアントがロックを取得できなくなり、長時間正常に動作できなくなります。少し短めに設定する必要があります。スレッドがロックを保持している場合、スレッドが有効期間を自動的に延長できるようにします。 方法3: RedLockに基づく分散ロックの実装上記の 2 つの点に応えて、antirez は Redlock アルゴリズムを設計しました。 Redis の作者 antirez は、分散ロックを実装するための公式 Redis ガイドである Redlock と呼ばれるより優れた実装を提供しました。 Redlock アルゴリズムの説明は、Redis の公式 Web サイトにあります: https://redis.io/topics/distlock 目的: 共有リソースへの相互排他アクセスそのため、antirezは、N個の完全に独立したRedisノード(Nは通常5に設定できます)に基づく新しい分散ロックアルゴリズムRedlockを提案しました。これは、N個のRedisデータが互いに通信できないことを意味します。 コード実装:- @Service( "RedisRedissonRedLockLockService をグラブする" )
- パブリッククラスGrabRedisRedissonRedLockLockServiceImplはGrabServiceを実装します{
-
- オートワイヤード
- プライベート RedissonClient red1;
- オートワイヤード
- プライベート RedissonClient redissonRed2;
- オートワイヤード
- プライベート RedissonClient redissonRed3;
-
- オートワイヤード
- 注文サービス注文サービス;
-
- @オーバーライド
- パブリックレスポンス結果 grabOrder( int orderId , int driverId){
- //キーを生成する
- 文字列 lockKey = (RedisKeyConstant.GRAB_LOCK_ORDER_KEY_PRE + orderId).intern();
- //赤いロック
- RLock rLock1 = redissonRed1.getLock(lockKey);
- RLock rLock2 = redissonRed2.getLock(lockKey);
- RLock rLock3 = redissonRed2.getLock(lockKey);
- RedissonRedLock rLock = 新しい RedissonRedLock(rLock1、rLock2、rLock3);
-
- 試す {
- rLock.ロック();
- // このコードは、キーのタイムアウトをデフォルトで30秒に設定し、10秒後に遅延させます
- システム。 out .println( "ユーザー: " +driverId+ " 注文取得ロジックを実行します" );
-
- ブール値 b = orderService.grab(orderId, driverId);
- もし(b) {
- システム。 out .println( "ユーザー: " + driverId + " 注文が正常に取得されました" );
- }それ以外{
- システム。 out .println( "ユーザー: " +driverId+ " 注文を取得できませんでした" );
- }
-
- ついに
- rLock.ロック解除();
- }
- 戻る ヌル;
- }
- }
Redlock アルゴリズムを実行しているクライアントは、ロック取得操作を完了するために次の手順を順番に実行します。 - 現在の時刻をミリ秒単位で取得します。
- N 個の Redis ノードに対してロック取得操作を順番に実行します。この取得操作は、値 driverId と有効期限 (たとえば、ロックの有効期間である PX30000) を含む、単一の Redis ノードに基づいてロックを取得する前のプロセスと同じです。 Redis ノードが利用できない場合でもアルゴリズムが継続して実行されるようにするため、ロック取得操作にはタイムアウトが設定されています。このタイムアウトは、ロックの有効時間よりもはるかに短くなっています (数十ミリ秒程度)。
- クライアントが Redis ノードからのロックの取得に失敗した場合は、すぐに次の Redis ノードを試行する必要があります。ここでの障害には、Redis ノードが利用できない、Redis ノードのロックがすでに他のクライアントによって保持されているなど、あらゆるタイプの障害が含まれます。
- 手順 1 で記録した時間を現在の時刻から減算して、ロック取得プロセス全体にかかった合計時間を計算します。クライアントが Redis ノードの大多数 (>= N/2+1) からロックを正常に取得した場合、たとえば、5 台のマシンのうち 3 台が正常にロックされた場合、ロックは成功したとみなされ、ロックの取得に費やされた合計時間がロックの有効時間を超えない場合、クライアントはロックが最終的に正常に取得されたと見なします。それ以外の場合、ロックの取得は最終的に失敗したとみなされます。
- 最終的にロックが正常に取得された場合、ロックの有効時間を再計算する必要があります。これは、ロックの初期有効時間から、手順 3 で計算されたロックの取得に費やされた時間を差し引いた値に等しくなります。
- 最終的にロックの取得に失敗した場合 (ロックを取得した Redis ノードの数が N/2+1 未満であるか、ロック取得プロセス全体がロックの初期有効期間よりも長くかかるため)、クライアントはすべての Redis ノードに対してロック解放操作を直ちに開始する必要があります (つまり、前に紹介した Redis Lua スクリプト)。上記の説明はロックを取得するプロセスについてのみ説明していますが、ロックを解除するプロセスは比較的単純です。クライアントは、その時点でノードがロックの取得に成功したかどうかに関係なく、すべての Redis ノードに対してロック解除操作を開始します。
要約するこれでRedis分散ロックの説明は終了です。使用する分散ロックの具体的なタイプは、会社のビジネスによって異なります。 RedLock は大規模トラフィック用の分散ロックを実装するために使用でき、redisson は小規模トラフィックに使用できます。分散ロックを実装するための Zookeeper については後ほど説明します。 |