ロック後も同時実行の問題は残りますか? Redis 分散ロック、本当に正しく使用されていますか?

ロック後も同時実行の問題は残りますか? Redis 分散ロック、本当に正しく使用されていますか?

新しく引き継いだプロジェクトでは、アカウントの不均衡により問題が発生する場合があります。前の技術担当の上司は、退職前に「調べたが原因がわからなかった、忙しくて解決できなかったのでフレームワークが原因かもしれない」と説明していましたが…

プロジェクトが完了したので、このような問題を解決する必要があります。すべての会計処理ロジックを整理した後、ようやく原因がわかりました。データベース内のホット アカウントに対する同時操作が原因でした。今回は、分散システムにおける Redis ベースの分散ロックについて説明します。ちなみに、問題の原因と解決策も分析してみましょう。

原因分析

システムの同時実行性は高くなく、ホットアカウントもありますが、それほど深刻ではありません。問題の根本は、同時実行性を人為的に生み出すシステム アーキテクチャ設計にあります。シナリオは次のとおりです。販売者がバッチでデータをインポートすると、システムが前処理を実行し、口座残高を増減します。

この時点で、別のスケジュールされたタスクもアカウントをスキャンして更新します。さらに、同じアカウントに対する操作がさまざまなシステムに分散され、ホットアカウントが出現します。

この問題の解決策として、アーキテクチャの観点から、会計システムを抽出して 1 つのシステムに集中させることが考えられます。すべてのデータベース トランザクションと実行順序は、会計システムによって調整および処理されます。技術的な観点から見ると、ホット スポット アカウントはロック メカニズムによってロックされる可能性があります。

この記事では、分散ロックに基づいてホットスポット アカウントを実装する方法について詳しく説明します。

ロック分析

Java のマルチスレッド環境では、通常、使用できるロックの種類がいくつかあります。

  • 一般的に使用される JVM メモリ モデル レベルのロックは、synchronized、Lock などです。
  • 楽観的ロック、悲観的ロックなどのデータベース ロック。
  • 分散ロック。

JVM メモリ レベルのロックは、複数のスレッドがグローバル変数にアクセスしたり変更したりするなど、単一のサービス内のスレッドのセキュリティを確保できます。しかし、システムがクラスターに展開されている場合、JVM レベルでのローカル ロックは無力になります。

悲観的ロックと楽観的ロック

上記のケースのように、ホット アカウントは分散システム内の共有リソースであり、通常はデータベース ロックまたは分散ロックを使用して解決します。

データベース ロックは、楽観的ロック悲観的ロックに分けられます。

悲観的ロックは、データベース (MySQL の InnoDB) によって提供される排他ロックに基づいて実装されます。トランザクションを実行すると、MySQL は select ... for update ステートメントを通じてクエリ結果セット内の各データ行に排他ロックを追加し、他のスレッドはレコードの更新および削除操作をブロックします。これにより、共有リソースの順次実行(変更)が実現されます。

楽観的ロックは悲観的ロックに相対的です。楽観的ロックでは、通常の状況ではデータが競合を引き起こさないと想定されるため、データが更新のために送信されると、データが競合しているかどうかを正式にチェックします。競合が発生した場合は、例外メッセージがユーザーに返され、ユーザーは対処方法を決定できます。楽観的ロックは、書き込みよりも読み取りが多いシナリオに適しており、プログラムのスループットを向上させることができます。楽観的ロックを実装する場合、通常は記録ステータスまたはバージョンの追加に基づいて実装されます。

悲観的なロック失敗シナリオ

プロジェクトでは悲観的ロックが使用されましたが、失敗しました。これは、悲観的ロックを使用する場合によくある誤解でもあります。以下で分析してみましょう。

悲観的ロックを使用する通常のプロセス:

  • 更新のために select ... を通じてレコードをロックします。
  • 新しい残高を計算し、金額を変更して保存します。
  • 実行が完了したらロックを解除します。

プロセスにおけるよくある間違い:

  • 口座残高を確認し、新しい残高を計算します。
  • 更新のために select ... を通じてレコードをロックします。
  • 金額を変更して保存します。
  • 実行が完了したらロックを解除します。

間違ったプロセスでは、たとえば、サービス A と B によって照会された残高が両方とも 100 で、A が 50 を差し引き、B が 40 を差し引き、その後 A がレコードをロックし、データベースを 50 に更新します。 A がロックを解除した後、B はレコードをロックし、データベースを 60 に更新します。明らかに、後者は前者の更新を上書きします。解決策は、ロックの範囲を拡大し、新しい残高を計算する前にロックを進めることです。

通常、悲観的ロックはデータベースに大きな負担をかけます。実際には、シナリオに応じて楽観的ロックまたは分散ロックを使用して実装されるのが一般的です。

それでは、本題に入り、Redis に基づく分散ロックの実装について説明しましょう。

Redis 分散ロックの実践演習

ここでは、Spring Boot、Redis、Lua スクリプトを例に、分散ロックの実装を説明します。プロセスを簡素化するために、この例の Redis は分散ロックとデータベースの両方の機能を想定しています。

シーン構築

クラスター環境で、同じアカウントの金額を操作するための基本的な手順は次のとおりです。

  • データベースからユーザーの金額を読み取ります。
  • プログラム修正量;
  • 次に、最新の金額をデータベースに保存します。

以下は、初期のロック解除と非同期処理から最終的な分散ロックを段階的に推論したものです。

基本的な統合とクラス構築

ロック解除処理のための基本的なビジネス環境を準備します。

まず、Spring Boot プロジェクトに関連する依存関係を導入します。

  1. <依存関係>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </依存関係>
  5. <依存関係>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </依存関係>

アカウントはエンティティ クラス UserAccount に対応します。

  1. 公共 クラスUserAccount {
  2.  
  3. //ユーザーID  
  4. プライベート文字列ユーザーID;
  5. //口座残高 
  6. プライベート 整数量;
  7.  
  8. //口座金額を追加 
  9. 公共  void addAmount( int金額) {
  10. this .amount = this .amount + amount;
  11. }
  12. // コンストラクタとゲッター/セッターを省略 
  13. }

スレッド実装クラス AccountOperationThread を作成します。

  1. 公共 クラスAccountOperationThreadは Runnableを実装します{
  2.  
  3. プライベート ファイナル 静的Logger ロガー = LoggerFactory.getLogger(AccountOperationThread.class ) ;
  4.  
  5. プライベート 静的 最終的なLong RELEASE_SUCCESS = 1L;
  6.  
  7. プライベート文字列ユーザーID;
  8.  
  9. プライベートRedis テンプレート <Object, Object> redis テンプレート;
  10.  
  11. パブリックアカウント操作スレッド(文字列userId、RedisTemplate<Object、Object> redisTemplate) {
  12. this .userId = ユーザーId;
  13. これは.redisTemplate = redisTemplate; です。
  14. }
  15.  
  16. @オーバーライド 
  17. 公共  void実行() {
  18. ロックなし();
  19. }
  20.  
  21. /**
  22. * ロック解除
  23. */  
  24. プライベート  void noLock() {
  25. 試す{
  26. ランダム random = new Random();
  27. //ビジネス処理のスレッドをシミュレートする 
  28. TimeUnit.MILLISECONDS.sleep(random.nextInt( 100 ) +1 );
  29. }キャッチ(InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. //シミュレーションデータベースからユーザーアカウントを取得する 
  33. ユーザーアカウント userAccount = (ユーザーアカウント) redisTemplate.opsForValue().get(userId);
  34. // 金額 +1  
  35. ユーザーアカウントに金額を追加します( 1 );
  36. logger.info(Thread.currentThread().getName() + " : ユーザーID : " + userId + " 金額 : " + userAccount.getAmount());
  37. //データベースへの保存をシミュレートする 
  38. redisTemplate.opsForValue() を設定します(userId、userAccount);
  39. }
  40. }

RedisTemplate のインスタンス化は Spring Boot に渡されます。

  1. @構成 
  2. 公共 クラスRedisConfig {
  3.  
  4. @ビーン 
  5. パブリックRedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  6. Redis テンプレートを新規作成します
  7. redisTemplate.setConnectionFactory(redisConnectionFactory);
  8. Jackson2JsonRedisSerializer<オブジェクト> jackson2JsonRedisSerializer =
  9. 新しいJackson2JsonRedisSerializer<>(Object. class );
  10. オブジェクトマッパー objectMapper =新しいオブジェクトマッパー();
  11. objectMapper.setVisibility(PropertyAccessor.ALL、JsonAutoDetect.Visibility.ANY);
  12. オブジェクトマッパーのデフォルトタイピングを有効にします(ObjectMapper.DefaultTyping.NON_FINAL);
  13. jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
  14. // 値とキーのシリアル化ルールを設定する 
  15. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  16. redisTemplate.setKeySerializer(新しいStringRedisSerializer());
  17. redisTemplate.afterPropertiesSet();
  18. redisTemplateを返します
  19. }
  20. }

最後に、マルチスレッド操作をトリガーする TestController を準備します。

  1. @レストコントローラ 
  2. 公共 クラスTestController {
  3.  
  4. プライベート ファイナル 静的Logger ロガー = LoggerFactory.getLogger(TestController. class );
  5.  
  6. プライベート 静的ExecutorService executorService = Executors.newFixedThreadPool( 10 );
  7.  
  8. オートワイヤード 
  9. プライベートRedis テンプレート <Object, Object> redis テンプレート;
  10.  
  11. @GetMapping ( "/テスト" )
  12. パブリック文字列test()InterruptedExceptionをスローします{
  13. // ユーザー user_001 を Redis に初期化します。アカウント残高は 0 です 
  14. redisTemplate.opsForValue().set( "user_001" 新しいUserAccount( "user_001" 0 ));
  15. // 同期テスト用に 10 個のスレッドを開始し、各スレッドはアカウントに 1 元を追加します 
  16. ( int i = 0 ; i < 10 ; i++) {
  17. logger.info( "スレッド i を作成=" + i);
  18. executorService.execute(新しいAccountOperationThread( "user_001" 、 redisTemplate));
  19. }
  20.  
  21. // メインスレッドは1秒間スリープし、スレッドの実行が完了するまで待機します 
  22. TimeUnit.MILLISECONDS.sleep( 1000 );
  23. // Redis で user_001 アカウントをクエリする 
  24. ユーザーアカウント userAccount = (UserAccount) redisTemplate.opsForValue().get( "user_001" );
  25. logger.info( "ユーザーID: " + userAccount.getUserId() + "金額: " + userAccount.getAmount());
  26. 戻る  "成功" ;
  27. }
  28. }

上記のプログラムを実行します。通常、スレッドは 10 個あり、各スレッドが 1 を加算するので、結果は 10 になるはずです。しかし、複数回実行すると、結果が大きく異なり、基本的に 10 より小さくなることがわかります。

  1. [プール- 1 -スレッド- 5 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 5 : ユーザーID: user_001 量: 1  
  2. [プール- 1 -スレッド- 4 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 4 : ユーザーID: user_001 量: 1  
  3. [プール- 1 -スレッド- 3 ] csredis.thread.AccountOperationThread : プール- 1 -スレッド- 3 : ユーザーID : user_001 量 : 1  
  4. [プール- 1 -スレッド- 1 ] csredis.thread.AccountOperationThread : プール- 1 -スレッド- 1 : ユーザーID : user_001 量 : 1  
  5. [プール- 1 -スレッド- 1 ] csredis.thread.AccountOperationThread : プール- 1 -スレッド- 1 : ユーザーID : user_001 量 : 2  
  6. [プール1 -スレッド2 ] csredis.thread.AccountOperationThread: プール1 -スレッド2 : ユーザー ID: user_001 量: 2  
  7. [プール- 1 -スレッド- 5 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 5 : ユーザーID: user_001 量: 2  
  8. [プール- 1 -スレッド- 4 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 4 : ユーザーID: user_001 量: 3  
  9. [プール- 1 -スレッド- 1 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 1 : ユーザーID: user_001 量: 4  
  10. [プール- 1 -スレッド- 3 ] csredis.thread.AccountOperationThread: プール- 1 -スレッド- 3 : ユーザーID: user_001 量: 5  
  11. [nio- 8080 -exec- 1 ] csredis.controller.TestController: ユーザーID: user_001 量: 5  

上記のログを例にとると、最初の 4 つのスレッドはすべて値を 1 に変更しました。つまり、次の 3 つのスレッドが以前の変更を上書きし、最終結果が 10 ではなく 5 になります。これは明らかに問題があります。

Redis 同期ロックの実装

上記の状況では、同じ JVM 内でスレッド ロックを行うことで解決できます。ただし、分散環境では JVM レベルのロックは実装できないため、ここでは Redis 同期ロックを使用できます。

基本的な考え方: 最初のスレッドが入ると、Redis にレコードが入力されます。後続のスレッドがリクエストを送信すると、Redis にレコードが存在するかどうかが判断されます。存在する場合は、ロック状態にあり、待機または戻ることを意味します。存在しない場合は、後続の業務処理に進みます。

  1.   /**
  2. * 1. リソースを取得するときに、リソースがロックされているかどうかを判断します。
  3. * 2. ロックされていない場合はプリエンプションが成功し、ロックが追加されます。そうでない場合は、ロックが解除されるまで待機します。
  4. * 3. 業務が完了したらロックを解除し、他のスレッドに渡します。
  5. * <p>
  6. * このソリューションでは、スレッドの取得とロックのプロセスがアトミック操作ではないため、同期の問題は解決されません。スレッド A がロックを取得する可能性がありますが、スレッド B もロックを取得する前にロックを取得します。
  7. */  
  8. プライベート  void redisLock() {
  9. ランダム random = new Random();
  10. 試す{
  11. TimeUnit.MILLISECONDS.sleep(random.nextInt( 1000 ) +1 );
  12. }キャッチ(InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. の間{
  16. オブジェクト lock = redisTemplate.opsForValue().get(userId + ":syn" );
  17. ロック == null場合
  18. // ロックを取得 -> ロック -> ループから抜け出す 
  19. logger.info(Thread.currentThread().getName() + ":ロックを取得" );
  20. redisTemplate.opsForValue().set(userId + ":syn" , "lock" );
  21. 壊す;
  22. }
  23. 試す{
  24. // ロックの取得を再試行するために 500 ミリ秒待機します 
  25. TimeUnit.MILLISECONDS.sleep( 500 );
  26. }キャッチ(InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. 試す{
  31. //シミュレーションデータベースからユーザーアカウントを取得する 
  32. ユーザーアカウント userAccount = (ユーザーアカウント) redisTemplate.opsForValue().get(userId);
  33. ユーザーアカウントがnull場合
  34. //金額を設定する 
  35. ユーザーアカウントに金額を追加します( 1 );
  36. logger.info(Thread.currentThread().getName() + " : ユーザーID : " + userId + " 金額 : " + userAccount.getAmount());
  37. //データベースへの保存をシミュレートする 
  38. redisTemplate.opsForValue() を設定します(userId、userAccount);
  39. }
  40. 最後に
  41. //ロックを解除する 
  42. redisTemplate.delete(userId + ":syn" );
  43. logger.info(Thread.currentThread().getName() + ":ロックを解除" );
  44. }
  45. }

while コード ブロックでは、まず対応するユーザー ID が Redis に存在するかどうかを判断します。そうでない場合は設定してロックしてください。そうであれば、ループを終了して待機を続けます。

上記のコードはロック機能を実装しているように見えますが、プログラムを実行すると、ロックされていないかのように並行性の問題があることがわかります。その理由は、取得とロックの操作がアトミックではないからです。たとえば、2 つのスレッドがロックが両方とも null であることを検出すると、両方のスレッドがロックしますが、同時実行の問題は依然として存在します。

Redis アトミック同期ロック

上記の問題に対処するために、ロックの取得とロックのプロセスをアトミックに処理することができます。 spring-boot-data-redis によって提供されるアトミック API に基づいて、次のことを実現できます。

  1. // このメソッドはredis命令を使用します: SETNXキー値 
  2. // 1. キーが存在しない場合は、set は正常に値を返し、setIfAbsent は true を返します。  
  3. // 2. キーが存在する場合、設定は失敗して null を返し、setIfAbsent は false を返します。  
  4. // 3.アトミック操作;  
  5. ブール値setIfAbsent(K var1, V var2);

上記のメソッドのアトミック操作は、Redis setnx コマンドのカプセル化です。次の例では、Redis で setnx を使用します。

  1. redis> SETNX mykey "こんにちは"  
  2. (整数) 1  
  3. redis> SETNX mykey "ワールド"  
  4. (整数) 0  
  5. redis> GET mykey
  6. "こんにちは"  

mykey が初めて設定されるときに、存在しない場合は、設定が成功したことを示す 1 を返します。 2 回目に mykey が設定され、すでに存在する場合は、設定が失敗したことを示す 0 が返されます。 mykey に対応する値を再度照会すると、それが最初に設定された値のままであることがわかります。つまり、redis の setnx は、一意のキーが 1 つのサービスによってのみ正常に設定されることを保証します。

上記の API と基本原理を理解した後、次のスレッドの実装コードを見てみましょう。

  1. /**
  2. * 1. アトミック操作ロック
  3. * 2. 競合するスレッドはループで再試行してロックを取得します
  4. * 3. 業務終了後はロックを解除する
  5. */  
  6. プライベート  voidアトミック性RedisLock() {
  7. //Spring Data Redis でサポートされているアトミック操作 
  8. (!redisTemplate.opsForValue().setIfAbsent(userId + " :syn" , "lock" )) {
  9. 試す{
  10. // ロックの取得を再試行するために 100 ミリ秒待機します 
  11. TimeUnit.MILLISECONDS.sleep( 100 );
  12. }キャッチ(InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. logger.info(Thread.currentThread().getName() + ":ロックを取得" );
  17. 試す{
  18. //シミュレーションデータベースからユーザーアカウントを取得する 
  19. ユーザーアカウント userAccount = (ユーザーアカウント) redisTemplate.opsForValue().get(userId);
  20. ユーザーアカウントがnull場合
  21. //金額を設定する 
  22. ユーザーアカウントに金額を追加します( 1 );
  23. logger.info(Thread.currentThread().getName() + " : ユーザーID : " + userId + " 金額 : " + userAccount.getAmount());
  24. //データベースへの保存をシミュレートする 
  25. redisTemplate.opsForValue() を設定します(userId、userAccount);
  26. }
  27. 最後に
  28. //ロックを解除する 
  29. redisTemplate.delete(userId + ":syn" );
  30. logger.info(Thread.currentThread().getName() + ":ロックを解除" );
  31. }
  32. }

コードを再度実行すると、結果が正しいことがわかります。これは、分散スレッドを正常にロックできることを意味します。

Redis 分散ロック デッドロック

上記コードの実行結果は問題ありませんが、アプリケーションが異常終了し、最終的にロックを解除するメソッドを実行する時間がない場合、他のスレッドはロックを取得できなくなります。

このとき、setIfAbsent のオーバーロード メソッドを使用できます。

  1. ブール型setIfAbsent(K var1, V var2, long var3, TimeUnit var5);

この方法に基づいて、ロックの有効期限を設定できます。こうすることで、ロックを取得したスレッドがクラッシュした場合でも、Redis 内のデータの有効期限が切れた後は他のスレッドが正常にロックを取得できるようになります。

サンプルコードは次のとおりです。

  1. プライベート  voidアトミック性およびExRedisLock() {
  2. 試す{
  3. //Spring Data Redis でサポートされているアトミック操作。有効期限を 5 秒に設定します。  
  4. while (!redisTemplate.opsForValue().setIfAbsent(userId + ":syn"
  5. System.currentTimeMillis() + 5000 , 5000 , TimeUnit.MILLISECONDS)) {
  6. // ロックの取得を再試行するために 100 ミリ秒待機します 
  7. logger.info(Thread.currentThread().getName() + ": ループ内でロックを取得しようとしています" );
  8. TimeUnit.MILLISECONDS.sleep( 1000 );
  9. }
  10. logger.info(Thread.currentThread().getName() + ":ロックを取得--------" );
  11. // ここでアプリケーションがクラッシュし、プロセスが終了し、最終的に実行できなくなります。  
  12. スレッド.currentThread().interrupt();
  13. // ビジネス ロジック...  
  14. }キャッチ(InterruptedException e) {
  15. e.printStackTrace();
  16. 最後に
  17. //ロックを解除する 
  18. (!Thread.currentThread().isInterrupted())の場合{
  19. redisTemplate.delete(userId + ":syn" );
  20. logger.info(Thread.currentThread().getName() + ":ロックを解除" );
  21. }
  22. }
  23. }

ビジネスタイムアウトとデーモンスレッド

上記の Redis タイムアウトを追加すると問題は解決するように見えますが、新たな問題が発生します。

たとえば、通常の状況では、スレッド A は 5 秒以内にビジネス処理を完了できますが、場合によっては 5 秒以上かかることがあります。タイムアウトを 5 秒に設定すると、スレッド A はロックを取得しますが、ビジネス ロジックの処理には 6 秒かかります。この時点では、スレッド A はまだ通常のビジネス ロジックを実行しており、スレッド B がロックを取得しています。スレッドAが処理を終了すると、スレッドBのロックを解除することができます。

上記のシナリオには 2 つの問題があります。

  • まず、スレッド A とスレッド B が同時に実行される可能性があり、その結果、同時実行性の問題が発生します。
  • 次に、スレッド A がスレッド B のロックを解除し、一連の悪循環を引き起こす可能性があります。

もちろん、Redis で値を設定することで、ロックがスレッド A に属しているか、スレッド B に属しているかを判別できます。しかし、注意深く分析すると、この問題の本質は、スレッド A がビジネス ロジックを実行するのにかかる時間がロック タイムアウト期間を超えていることにあることがわかります。

その場合、解決策は 2 つあります。

  • まず、ロックが解除される前にビジネス コードを実行できるように、タイムアウトを十分に長く設定します。
  • 次に、ロックにデーモン スレッドを追加して、期限が切れそうでまだ解放されていないロックの時間を延長します。

最初の方法では、ほとんどの場合、銀行全体のビジネス ロジックの時間消費とタイムアウト期間の設定が必要になります。

2 番目の方法は、次のデーモン スレッド メソッドを使用して、ロック タイムアウトを動的に増やすことです。

  1. 公共 クラスDaemonThreadはRunnableを実装します{
  2. プライベート ファイナル 静的Logger ロガー = LoggerFactory.getLogger(DaemonThread.class ) ;
  3.  
  4. // デーモン化が必要かどうか。メインスレッドが閉じられると、デーモンスレッドは終了します。  
  5. プライベート 不安定な ブール値デーモン = true ;
  6. // ガーディアンロック 
  7. プライベート文字列 lockKey;
  8.  
  9. プライベートRedis テンプレート <Object, Object> redis テンプレート;
  10.  
  11. パブリックDaemonThread(String lockKey, RedisTemplate<Object, Object> redisTemplate) {
  12. .lockKey = lockKey ;
  13. これは.redisTemplate = redisTemplate; です。
  14. }
  15.  
  16. @オーバーライド 
  17. 公共  void実行() {
  18. 試す{
  19. while (デーモン) {
  20. 長い時間 = redisTemplate.getExpire(lockKey, TimeUnit.MILLISECONDS);
  21. // 残りの有効期間が1秒未満の場合は、寿命を延長します 
  22. (時間 < 1000 )の場合{
  23. logger.info( "デーモンプロセス: " + Thread.currentThread().getName() + "ロック時間を 5000 ミリ秒延長" );
  24. redisTemplate.expire(lockKey, 5000 , TimeUnit.MILLISECONDS);
  25. }
  26. TimeUnit.MILLISECONDS.sleep( 300 );
  27. }
  28. logger.info( "デーモン: " + Thread.currentThread().getName() + "閉じられました" );
  29. }キャッチ(InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33.  
  34. // メインスレッドはendをアクティブに呼び出します 
  35. 公共  void stop() {
  36. デーモン = false ;
  37. }
  38. }

上記のスレッドは、300 ミリ秒ごとに Redis のロック タイムアウトを取得します。 1秒未満の場合は5秒延長されます。メイン スレッドがシャットダウンを呼び出すと、デーモン スレッドもシャットダウンされます。

メインスレッド内の関連コード実装:

  1. プライベート  voidデーモンRedisLock() {
  2. //ガーディアンスレッド 
  3. デーモンスレッド デーモンスレッド = null ;
  4. //Spring Data Redis でサポートされているアトミック操作。有効期限を 5 秒に設定します。  
  5. 文字列 uuid = UUID.randomUUID().toString();
  6. 文字列値 = Thread.currentThread().getId() + ":" + uuid;
  7. 試す{
  8. while (!redisTemplate.opsForValue().setIfAbsent(userId + ":syn" , value, 5000 , TimeUnit.MILLISECONDS)) {
  9. // ロックの取得を再試行するために 100 ミリ秒待機します 
  10. logger.info(Thread.currentThread().getName() + ": ループ内でロックを取得しようとしています" );
  11. TimeUnit.MILLISECONDS.sleep( 1000 );
  12. }
  13. logger.info(Thread.currentThread().getName() + ":ロックを取得----" );
  14. // デーモンスレッドを開始する 
  15. daemonThread =新しいDaemonThread(userId + ":syn" , redisTemplate);
  16. スレッド thread = new Thread(daemonThread);
  17. スレッドを開始します。
  18. // ビジネス ロジックは 10 秒間実行されます...  
  19. TimeUnit.MILLISECONDS.sleep( 10000 );
  20. }キャッチ(InterruptedException e) {
  21. e.printStackTrace();
  22. 最後に
  23. //ロックを解除します。ここでもアトミック操作が必要です。これについては、今後 Redis + Lua で説明します。  
  24. 文字列結果 = (文字列) redisTemplate.opsForValue().get(userId + ":syn" );
  25. if (値.equals(結果)) {
  26. redisTemplate.delete(userId + ":syn" );
  27. logger.info(Thread.currentThread().getName() + ":ロックを解除-----" );
  28. }
  29. //デーモンスレッドを閉じる 
  30. デーモンスレッドがnull場合
  31. デーモンスレッドを停止します。
  32. }
  33. }
  34. }

ロックを取得した後、デーモン スレッドが開始され、最終的に閉じられます。

Luaスクリプトに基づく実装

上記のロジックでは、spring-boot-data-redis が提供するアトミック操作に基づいて、ロックの判断と実行のアトミック性を保証します。 Spring Boot 以外のプロジェクトでは、Lua スクリプトに基づいて実装できます。

まず、ロックとロック解除用の Lua スクリプトと対応するDefaultRedisScriptオブジェクトを定義し、 RedisConfig構成クラスに次のインスタンス化コードを追加します。

  1. @構成 
  2. 公共 クラスRedisConfig {
  3.  
  4. //スクリプトをロックする 
  5. プライベート 静的 最終文字列 LOCK_SCRIPT = " if redis.call('setnx',KEYS[1],ARGV[1]) == 1 " +
  6. " その後 redis.call('expire',KEYS[1],ARGV[2]) " +
  7. " 1 を返す " +
  8. " そうでなければ 0 を返して終了 " ;
  9. プライベート 静的 最終文字列 UNLOCK_SCRIPT = "redis.call('get', KEYS[1]) == ARGV[1] の場合、redis.call を返します" +
  10. 「('del', KEYS[1]) そうでなければ0を返す 終了」 ;
  11.  
  12. // ... 一部のコードは省略 
  13.   
  14. @ビーン 
  15. パブリックDefaultRedisScript<ブール値> lockRedisScript() {
  16. DefaultRedisScript<ブール値> defaultRedisScript =新しいDefaultRedisScript<>();
  17. defaultRedisScript.setResultType( Boolean.class );
  18. デフォルトのRedisScript.setScriptText(LOCK_SCRIPT);
  19. defaultRedisScriptを返します
  20. }
  21.  
  22. @ビーン 
  23. パブリックDefaultRedisScript<Long>RedisScriptのロック解除() {
  24. DefaultRedisScript<Long> defaultRedisScript =新しいDefaultRedisScript<>();
  25. defaultRedisScript.setResultType( Long.class );
  26. デフォルトのRedisScript.setScriptText(UNLOCK_SCRIPT);
  27. defaultRedisScriptを返します
  28. }
  29. }

次に、 AccountOperationThreadクラスに新しい構築メソッドを作成し、上記の 2 つのオブジェクトをクラスに渡します (デモのこの部分は省略されています)。その後、 RedisTemplateに基づいて呼び出すことができます。変更されたコードは次のとおりです。

  1. プライベート  void deamonRedisLockWithLua() {
  2. //ガーディアンスレッド 
  3. デーモンスレッド デーモンスレッド = null ;
  4. //Spring Data Redis でサポートされているアトミック操作。有効期限を 5 秒に設定します。  
  5. 文字列 uuid = UUID.randomUUID().toString();
  6. 文字列値 = Thread.currentThread().getId() + ":" + uuid;
  7. 試す{
  8. while (!redisTemplate.execute(lockRedisScript, Collections.singletonList(userId + ":syn" ), value, 5 )) {
  9. // ロックの取得を再試行するために 1000 ミリ秒待機します 
  10. logger.info(Thread.currentThread().getName() + ": ループ内でロックを取得しようとしています" );
  11. TimeUnit.MILLISECONDS.sleep( 1000 );
  12. }
  13. logger.info(Thread.currentThread().getName() + ":ロックを取得----" );
  14. // デーモンスレッドを開始する 
  15. daemonThread =新しいDaemonThread(userId + ":syn" , redisTemplate);
  16. スレッド thread = new Thread(daemonThread);
  17. スレッドを開始します。
  18. // ビジネス ロジックは 10 秒間実行されます...  
  19. TimeUnit.MILLISECONDS.sleep( 10000 );
  20. }キャッチ(InterruptedException e) {
  21. logger.error( "例外" , e);
  22. 最後に
  23. //Luaスクリプトを使用する: 最初にロックが設定されているかどうかを確認し、削除を実行します 
  24. // キーが存在し、現在の値が期待値と等しい場合は、キーを削除します。キーが存在し、現在の値 != 期待値の場合は 0 を返します。  
  25. 長い結果 = redisTemplate.execute(unlockRedisScript、Collections.singletonList(userId + ":syn" )、値);
  26. logger.info( "redis のロック解除:{}" 、 RELEASE_SUCCESS.equals(result));
  27. (RELEASE_SUCCESS.equals(結果))の場合{
  28. デーモンスレッドがnull場合
  29. //デーモンスレッドを閉じる 
  30. デーモンスレッドを停止します。
  31. logger.info(Thread.currentThread().getName() + ":ロックを解除---" );
  32. }
  33. }
  34. }
  35. }

while ループでのロックと finally でのロックの解放は、どちらも Lua スクリプトに基づいて実装されています。

Redisロックのその他の要素

上記の例に加えて、Redis 分散ロックを使用する場合は、次の状況と解決策も考慮できます。

Redisのロックは再入可能ではない

スレッドがロックを保持したまま再度ロックを要求する場合、ロックがスレッドによる複数のロックをサポートしていれば、ロックは再入可能になります。再入不可能なロックが再度ロックされると、ロックがすでに保持されているため、ロックは失敗します。 Redis はロックの再入力をカウントし、ロック時に 1 を加算し、ロック解除時に 1 を減算し、カウントが 0 に戻るとロックを解除します。

再入可能ロックは効率的ですが、コードの複雑さが増すため、ここでは例を挙げません。

ロックが解除されるのを待っています

一部のビジネス シナリオでは、システムがロックされていることが判明すると、直接戻ります。しかし、シナリオによっては、クライアントはロックを取得する前にロックが解放されるのを待つ必要があります。上記の例は後者に属します。ロックの解放を待つための解決策も 2 つあります。

  • クライアント ポーリング: ロックが取得されない場合は、しばらく待ってから成功するまで再試行します。上記の例はこのように実装されています。この方法の欠点も明らかです。同時実行量が多い場合、サーバーのリソース消費量が増加し、サーバーの効率に影響します。
  • Redis のサブスクリプションとパブリッシュ機能を使用します。ロックの取得に失敗した場合はロック解除メッセージをサブスクライブし、ロックの取得と解除に成功した場合は解除メッセージを送信します。

クラスタ内のアクティブ/スタンバイの切り替えとスプリットブレイン

マスターとスレーブの同期を含む Redis クラスターの展開モードでは、マスター ノードがハングアップすると、スレーブ ノードがマスター ノードに昇格されます。クライアント A がマスター ノードを正常にロックしたが、コマンドがスレーブ ノードにまだ同期されていない場合、マスター ノードはクラッシュし、スレーブ ノードがマスター ノードに昇格されます。新しいマスター ノードにはロックされたデータは含まれません。この場合、クライアント B はセッションを正常にロックし、同時実行シナリオが発生する可能性があります。

クラスターでスプリットブレインが発生すると、Redis マスターノードはスレーブノードおよびセンチネルクラスターとは異なるネットワークパーティションに存在します。センチネル クラスターはマスターの存在を感知できないため、スレーブ ノードをマスター ノードに昇格します。この時点では、2 つの異なるマスター ノードが存在します。これにより、同時実行の問題も発生します。 Redis Cluster の展開方法も同様です。

まとめ

本番環境で問題が発生し、原因をトラブルシューティングし、解決策を見つけ、最終的にRedisの分散に基づいて詳細な調査を行うことが、学習プロセスです。

同時に、インタビューを受けたり、分散共有リソースの解決方法を尋ねられたりするときはいつでも、「Redis に基づいて分散ロックを実装する」と口走りますが、この記事の学習を通じて、Redis の分散ロックは万能ではなく、使用の過程でタイムアウト、デッドロック、ロックの誤解、クラスター リーダーの選出/ブレイン スプリットなどの問題にも注意する必要があることがわかります。

Redis は高性能であることで知られていますが、分散ロックを実装するプロセスにはまだいくつかの問題があります。したがって、Redis ベースの分散ロックは同時実行の問題を大幅に軽減できますが、同時実行を完全に防止するには、データベース レベルから開始する必要があります。

<<:  ローコードおよびノー​​コードソフトウェアが開発者の仕事を再定義

>>:  エンタープライズハイブリッドクラウドの将来はどうなるのでしょうか?

推薦する

ガートナー・マジック・クアドラント 2022: AWS、マイクロソフト、グーグル、オラクル、アリババクラウドがトップにランクイン

巨大なクラウド インフラストラクチャおよびプラットフォーム サービス市場でシェアを獲得することは、エ...

[高速韓国VPS] 2018年最速の速度を誇る韓国VPSのおすすめ、ファイリング不要/直接接続/大容量

韓国のVPSの推奨、高速な韓国のVPS、最速の韓国のVPS、最速の韓国のVPS... 韓国は中国本土...

UEO はウェブサイトのキーワードランキングの未来でしょうか?

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますまず、タイ...

政府がオンライン世論を監視する際に直面する主な問題

近年、さまざまなタイプのネット世論事件が頻発しており、ネット世論活動は国家レベルの注目を集めています...

1か月間の業界主要キーワードからホームページSEO実践(YahooとGoogle)

ケースクライアント: 北京性器いぼ治療ネットワーク ウェブサイト: http://www.kindi...

クラウド3.0の3つの大きな課題

[51CTO.com クイック翻訳] Cloud 1.0 は、仮想化ベースの Infrastruct...

JVMがバイトコード命令をどのように消化するかを見る

記事を書いてタイトルを考えるのは本当に頭の痛い作業です。文章はより技術的であり、堅苦しすぎると思われ...

Sina Weiboランキング(Weibo SEO)のヒント共有

Weiboの検索ランキングが徐々に人気になってきています。あなたのWeiboユーザー名のランキングが...

#RHS-$20/L5420/16G メモリ/1T ハードディスク/5IP/バージニア (米国)

Reliablehostingservices が素晴らしい情報を公開しました。L5420 といくつ...

ウェブサイトデータ分析に関する雑談

データ分析その他マーケティングの役職や職務には、一般的にデータの機密性に関する潜在的な要件がいくつか...

小規模ウェブサイトのキーワードをランク​​付けする4つのステップ

小規模ウェブサイトのキーワードをランク​​付けする4つのステップ私のウェブサイトは 9 月 2 日か...

PRを示す方法とPRを計算するアルゴリズム

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますGoogl...

Apple Chinaが新しいiCloudセンターを発表:中国人のデータはすべて中国に残る

[[219794]]さまざまな要素を考慮し、Appleは最終的に中国にiCloudデータセンターを構...

5G時代が加速しています。エッジ コンピューティングが「コア」コンピューティングに取って代わるのはいつでしょうか?

5G時代では、接続されるデバイスの数が大幅に増加し、ネットワークのエッジで膨大な量のデータが生成され...