blog

Spring Boot 2.X実践編 - シンプルな分散ロックを実装する

Spring BootとRedisの連携でも触れましたが、プロセス間で共有されるデータについては、ダーティデータの発生をロックによって回避する必要があり、Redisのシングルスレッドである特性を活かし...

Mar 4, 2020 · 10 min. read
シェア

コードクラウド:

Spring BootのRedisとの統合で述べたように、プロセス間で共有されるデータについては、ロックによってダーティなデータを回避する必要があり、 Redisのシングルスレッドの性質を利用して共有データのロックと解放を実装できます。この章では、単純な分散ロックの実装方法に焦点を当てます。

Javaアプリケーションを開発する場合、共有データ・リソースに対するマルチスレッドでの変更は、synchronizedまたはjava.util.concurrentパッケージのロックによって実現できます。分散システムでは、これらのスレッド間ロックツールは、異なるマシン上のプロセスには効果がありません。

分散システムでは、異なるマシン上のサービスが同時に共有データリソースにアクセスする可能性があり、複数のサービスが同時に書き込みと読み込みを行うと、異なるクライアントが取得したデータに一貫性がなくなり、最終的なデータに誤りが生じます。例えば、スパイクイベントでは100のアイテムがありますが、最終的には数十の売れすぎアイテムがあるかもしれません。

このような場合、1つのサービスだけが同時に共有データにアクセスすることを保証し、JVM全体で相互排除メカニズムを使用して共有リソースへのアクセスを制御するために、 分散ロックを 導入する必要があります。分散ロックには、データベースベース、Zookeeper、Redis、Memcached、Chubbyなど様々な実装があります。

分散ロックには以下の条件が必要です:

  • 相互排他性:一度に1つのスレッドしか同じリソースにアクセスできないことを保証します。
  • 高可用性と高パフォーマンス:ロック取得とロック解除のための高可用性と高パフォーマンス
  • ロック解除メカニズム:ロックを取得したスレッドがロックを解除せずにハングし、デッドロックになるのを防ぐため。
  • ノンブロッキング・ロック:ロックが取得されるまで待つのではなく、ロックを取得せずに失敗を返します。
  • 再エントリー:ロックの有効期限が切れたり解放されたりした後、そのスレッドはデータ・エラーを発生させることなくロックの取得を継続できます。

, シンプルなRedisロックの実装

Redisロック入門

ロックを

SET resource-name anystring NX EX max-lock-time Redisは、Redisのシングルスレッドの性質を利用し、アトミック操作のコマンドを使用することで、シンプルな分散ロックを実装しています。

# Redis  
SET key value [EX seconds] [PX milliseconds] [NX|XX]

Redisバージョン2.6.12以降のSETパラメータの説明:

  • EX、NX、PX、XXがない場合、キーがすでに存在する場合は、その型に関係なく値の値を上書きし、存在しない場合は、新しいキーと値を作成します。
  • EX秒:有効期限を秒に設定、SETキー値EX秒はSETEXキー秒値と同等。
  • PX millisecond : 鍵の有効期限をミリ秒単位で設定します。 SET key value EX second SETEX key second value 効果は .
  • XX : すでにキーが存在する場合にのみキーを設定します。
  • 値の値:ロックを解除する唯一のパスワードとしてランダムな文字列を追加する必要があり、期限切れのロックを保持するスレッドが誤って他のスレッドのロックを削除するのを防ぎます。

クライアントは上記のコマンドを実行します:

  • サーバーがOKを返せば、このクライアントはロックを取得します。
  • サーバーがNILを返した場合、クライアントはロックの取得に失敗したことになり、後で再試行することができます。

設定した有効期限に達すると、ロックは自動的に解除されます。

ロックを解除

DELコマンドを使用すると、期限切れのロックを保持しているスレッドが他のスレッドからロックを削除してしまうからです。Luaスクリプトを使用してロックを解放するのは、渡されたキーと値がRedisのものとまったく同じ場合に限ります。

# メソッドを使ってロックを解除する簡単なluaコマンド EVAL ...script... 1 resource-name token-value コマンドを使って
if redis.call("get",KEYS[1]) == ARGV[1]
then
 return redis.call("del",KEYS[1])
else
 return 0
end

Redisロック入門

新しいプロジェクト12-redis-lockを作成し、以下の依存関係を導入してください:

dependencies {
 implementation 'org.springframework.boot:spring-boot-starter-data-redis'
 implementation 'org.springframework.boot:spring-boot-starter-web'
 // AOP ロックのニーズを実装するための依存関係、アノテーション
 compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.1.13.RELEASE'
 testImplementation('org.springframework.boot:spring-boot-starter-test') {
 exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
 }
}

application.properties Redisサービスのアドレス、ポートパスワードなどを設定します:

# Redis host ip
spring.redis.host=wsl
# Redis サーバー接続ポート
spring.redis.port=6379
# Redis データベースインデックス
spring.redis.database=0
# Redis サーバー接続パスワード
spring.redis.password=springboot
# 接続プールの最大接続数
spring.redis.jedis.pool.max-active=8
# 接続プールの最大ブロック待機時間
spring.redis.jedis.pool.max-wait=-1
# コネクションプール内の最大空きコネクション数
spring.redis.jedis.pool.max-idle=8
# コネクション・プーリングにおける最小空きコネクション
spring.redis.jedis.pool.min-idle=0
# 接続タイムアウト
spring.redis.timeout=10

RedisLockUtil.java は、Redis ロックの取得と解放を実装します:

@Component
public class RedisLockUtil {
 Logger logger = LoggerFactory.getLogger(this.getClass());
 @Resource
 private StringRedisTemplate stringRedisTemplate;
 
 public boolean lock(String key, String value, long timeout, TimeUnit timeUnit) {
 // ロックには stringRedisTemplate.opsForValue().setIfAbsent(key, value, 15, TimeUnit.SECONDS);
 // Expiration.from(timeout, timeUnit) 期限と単位
 // RedisStringCommands.SetOption.SET_IF_ABSENT ,キーが存在しない場合のNXに相当する。
 Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
 connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
 Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
 return lockStat != null && lockStat;
 }
 public boolean unlock(String key, String value) {
 try {
 // ロックの解放にはLuaスクリプトを使用する。 key--value Redisと同じなのか?
 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
 Boolean unLockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
 connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
 key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8)));
 return unLockStat == null || !unLockStat;
 } catch (Exception e) {
 logger.error("ロック解除に失敗した key = {}", key);
 return false;
 }
 }
}

テストするには、新しい LockController.java を作成します:

@RestController
public class LockController {
 @Resource RedisLockUtil redisLockUtil;
 Logger logger = LoggerFactory.getLogger(this.getClass());
 @RequestMapping("/buy")
 public String buy(@RequestParam String goodId) {
 long timeout = 15;
 TimeUnit timeUnit = TimeUnit.SECONDS;
 // UUID   value
 String lockValue = UUID.randomUUID().toString();
 if (redisLockUtil.lock(goodId, lockValue, timeout, timeUnit)) {
 // ビジネス処理
 logger.info("業務処理のためのロックを取得する");
 try {
 // 10秒間Hibernateする
 Thread.sleep();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 //  
 if (redisLockUtil.unlock(goodId, lockValue)) {
 logger.error("redis分散ロック・アンロック例外キーは" + goodId);
 }
 return "購入の成功";
 }
 return "後でもう一度試してみて";
 }
}

複数のクライアントが同時に github.com/ アクセスしてプロジェクトを実行し、返される結果の違いを観察してください。

、ロックの獲得と解放を実現するためのアノテーションの使用

上記の例では、Redisの取得とリリースがRedisLockUtilクラスとしてカプセル化されていますが、それはまだ使用するのが面倒です、パブリックの前処理と後処理として抽出することができる多くの反復的なコードがあり、ビジネスコードは、特定のビジネスにのみ焦点を当てる必要がありますすることができます、ここではSpringのAOPカスタムアノテーションを介して実現する方法の簡単なデモンストレーションになります。Redisのロックの取得と解放。

ロックの取得と解放の上記のプロセスは、パブリックの前処理と後処理として抽出することができるため、ビジネスコードは、特定のビジネスに焦点を当てる必要があることができる、ここではSpringのAOPカスタムアノテーションRedisロックの取得と解放を介して達成する方法の簡単なデモンストレーションになります。

AOP入門

AOP(Aspect Oriented Program)は、オブジェクト指向プログラミングに追加された重要な機能で、Springの最も重要な機能の1つです。

AOPを使用すると、Redisのロック取得関数やロック解放関数など、システムコールに共通するロジックや関数をカプセル化できます。これにより、重複コードを減らし、異なるモジュール間の直接結合を減らし、将来の拡張と保守性を容易にします。たとえば、RedisLockUtil.javaに新しい関数が追加され、入力されるパラメータ値の型や数が変更された場合、AOPを使用せずに関連するコードをすべて変更する必要があります。

ファセット指向プログラミングの考え方では、関数はコアビジネスと周辺機能に分けられ、AOPは特に、システムのさまざまなモジュールに分散している横断的な関心事、すなわち周辺機能の問題に対処するために使用されます。

  • 基幹業務:ユーザーログイン、データベース操作など特定の業務。
  • 周辺機能:ロギング、モノの管理、セキュリティチェック、キャッシュなど。

AspectJはJava言語をベースとしたAOPフレームワークで、強力なAOP機能を提供しています。

新しいRedisLock.java(カスタムアノテーション)を作成します:

// メソッドがアノテーション可能であることを示す
@Target({ElementType.METHOD})
// 注釈保持の長さ、ランタイム保持
@Retention(RetentionPolicy.RUNTIME)
// 自動継承
@Inherited
@Documented
public @interface RedisLock {
 /** ロックのキー値は、空でない文字列でなければならない。 */
 @NotNull
 @NotEmpty
 String key();
 /** ロック値 */
 String value() default "";
 /** デフォルトのロック有効期限デフォルト 15 */
 long expire() default 15;
 /** ロックの有効期限の時間単位で、デフォルトは秒。 */
 TimeUnit timeUnit() default TimeUnit.SECONDS;
}

次に、RedisLockAspect.java を実装し、@RedisLock アノテーションを使用するメソッドに前処理と後処理を追加します:

@Component
@Aspect
public class RedisLockAspect {
 Logger logger = LoggerFactory.getLogger(this.getClass());
 @Resource
 private RedisLockUtil redisLockUtil;
 private String lockKey;
 private String lockValue;
 /**   RedisLock.javaこれは @RedisLock アノテーションは */
 @Pointcut("@annotation(org.xian.lock.RedisLock)")
 public void pointcut() {
 }
 @Around(value = "pointcut()")
 public Object around(ProceedingJoinPoint joinPoint) {
 //   @RedisLock  
 MethodSignature signature = (MethodSignature) joinPoint.getSignature();
 Method method = signature.getMethod();
 RedisLock redisLock = method.getAnnotation(RedisLock.class);
 // アノテーションでキーと等価な値を取得する
 lockKey = redisLock.key();
 lockValue = redisLock.value();
 if (lockValue.isEmpty()) {
 lockValue = UUID.randomUUID().toString();
 }
 try {
 Boolean isLock = redisLockUtil.lock(lockKey, lockValue, redisLock.expire(), redisLock.timeUnit());
 logger.info("{} ロックを取得すると {} ", redisLock.key(), isLock);
 if (!isLock) {
 // ロックの取得に失敗した
 logger.debug("ロックの取得に失敗した {}", redisLock.key());
 // 例外クラスとそのインターセプターをカスタマイズすることができる。 Spring Boot 2.X  --RESTful API グローバル例外処理
 // https://../---2/#/-?=--2x-%%%%%%--pi-%%%%%%%%%%%%%%%%%%86
 //   @AfterThrowing: 例外をスローする機能が強化された。ThrowsAdvice
 throw new RuntimeException("ロックの取得に失敗した");
 } else {
 try {
 // ロックの取得に成功、処理する
 return joinPoint.proceed();
 } catch (Throwable throwable) {
 throw new RuntimeException("システム例外");
 }
 }
 } catch (Exception e) {
 throw new RuntimeException("システム例外");
 }
 }
 @After(value = "pointcut()")
 public void after() {
 //  
 if (redisLockUtil.unlock(lockKey, lockValue)) {
 logger.error("redis分散ロック・アンロック例外キーは {}", lockKey);
 }
 }
}

LockController.java 追加

@RequestMapping("/buybuybuy")
@RedisLock(key = "lock_key", value = "lock_value")
public String buybuybuy(@RequestParam(value = "goodId") String goodId) {
 try {
 Thread.sleep();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 return "購入の成功";
}

アクセスして実行して ください。

欠点は、キーの値を渡すためにRedisキャッシュに表示される次のコードのようにすることはできませんが、この機能は、 Springの式言語を介して 利用可能ですので、カスタムアノテーションは、SpEL式の解析をサポートするために、ここで紹介されていません。

//   delete(@RequestParam(value = "username") String username)ユーザー名を key
@CacheEvict(key = "#username")
public User delete(@RequestParam(value = "username") String username) {
 User user = select(username);
 userRepository.delete(user);
 return user;
}

概要

Redisのスタンドアロン版で分散ロックの取得と解放を実現するための簡単な紹介ですが、Redisのクラスタ型は実際のビジネス展開にはあまり適用できないため、状況に応じてRedisson、RedLockフレームワークを利用することができます。

カスタムアノテーションの実装への簡単な紹介は、欠点はここに実装されていないSpEL式構文解析、および導入のSpringのAOPの重要な概念は比較的簡単ですが、次のリンクは、私が見たものです、ブログ記事への良い導入で書かれた、あなたはコードに対する情報を読むことができますし、材料の理解を深めるために拡張 。

doc.redisfans.com/string/set....

www.ibm.com/developerwo...

Read next

パンダのヒントを使う

最近、Pandasを使ってExcelファイルを操作することがよくあります。 だから、ここでプロセスの彼らの通常の使用の簡単な記録です。 ここでは簡略化のため、Excelファイルを2つ用意しただけで、内容は超シンプルです。 それぞれのExcelファイルですが、単純に言語と数学の2つのSheetを取得します。 このコードは、複数のシートのためのものです...

Mar 4, 2020 · 3 min read