SpringBootがRedisの分散ロックを利用して並行処理の問題を解決することについて
問題の背景
今日のアプリケーション・アーキテクチャでは、サービスの安定性を確保するために、多くのサービスが複数のコピーで実行されています。あるサービスインスタンスがハングアップしても、他のサービスはリクエストを受信することができます。例えば、あるインターフェースの処理ロジックは次のようなものです。リクエストを受信したら、まずDBに問い合わせて関連データがあるかどうかを確認し、なければデータを挿入し、あればデータを更新します。このとき、バックエンドのサービスインスタンスに同じN個のリクエストが同時に送られると、データの挿入が重複してしまう。
解決方法
上記の問題に対する一般的な解決策は、分散ロックを使用することである。しかし、サービスが複数のインスタンスに展開されている場合、サービスは分散され、各プロセスは独立している。この場合、グローバルロックの場所を設定し、各プロセスは何らかの方法でグローバルロックを取得し、ロックを取得した後にビジネスロジックのコードを実行し、ロックを取得しない場合は実行をスキップすることができるようにすることができる。このグローバル・ロックが分散ロックと呼ばれるものです。分散ロックは一般に3つの方法で実装される。1.データベースの楽観的ロック、2.Redisベースの分散ロック、3.ZooKeeperベースの分散ロック。
ここでは、Redisベースの分散ロックが分散並行処理問題を解決するためにどのように使用できるかを説明する。Redisはグローバルロックを取得する場所として機能し、各インスタンスはリクエストを受けるとまずRedisからロックを取得し、ロックを取得するとビジネスロジックのコードを実行し、ロックを争奪しない場合は実行を中断します。
主な実装方針。
Redisロックは、主にRedis setnxコマンドを使用して実装されます。
ロックコマンドです。SETNX key value:キーを設定し、キーが存在しない場合は成功を、それ以外の場合は失敗を返します。KEYはロックの一意な識別子で、一般に業務に応じて命名される。valueはロックが誤解されないようにUUIDで識別されるのが一般的である。
ロック解除のコマンドです。DEL keyは、キーと値のペアを削除してロックを解放し、他のスレッドがSETNXコマンドでロックを取得できるようにします。
ロックのタイムアウト EXPIRE key timeout: キーのタイムアウトを設定し、明示的にロックを解除しなくても一定時間後に自動的にロックが解除されるようにし、リソースが永遠にロックされることを防ぐ。
信頼性
分散ロックを確実に利用するためには、ロック実装が少なくとも4つの条件を同時に満たすようにする必要があります。
- 相互排他性。あるマシンの1つのスレッドだけが、任意の瞬間にロックを保持できることを保証するもの。
- デッドロックが発生しない。あるクライアントがロックを保持中にクラッシュし、積極的にロックを解除しない場合でも、後続の他のクライアントがロックを追加できることを保証します。
- ノンブロッキング。ロックが取得されないとすぐにロック失敗を返します。
- ロックとアンロックは同じクライアントが行う必要があります。クライアント自身は、他の人が追加したロックをアンロックすることはできません。
Redisの分散ロックを利用したSpringBootの統合
ビジネスロジックの実行前にロックし、ビジネスロジックの実行後にアンロックするためのRedisLockユーティリティクラスを書きました。SpringBootのバージョンが2.xの場合、コメントにあるコードでロック有効期限を設定しながらロックを追加することができます。SpringBootのバージョンが2.x以下であれば、Luaスクリプトを使用して、操作のアトミック性を確保することをお勧めします。簡単のため、以下のように記述します。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util;
import java.util.concurrent.TimeUnit;
/**
* @description: Redis distributed lock implementation tool class
* @author: qianghaohao
* @time: 2021/7 *@Component
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
/**
* Get the lock
*
* @param lockKey lock
* @param identity identity (ensures that the lock will not be released by anyone else)
* @param expireTime The expiration time of the lock (in seconds)
* @return
* public boolean lock(String lockKey, String identity, long expireTime) {
// Since we have a low version of springboot, 1.5.9, we don't support the following way of writing
// return redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS);
if (redisTemplate.opsForValue().setIfAbsent(lockKey, identity)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
return true;
}
return false;
}
/**
* Release the lock
*
* @param lockKey lock
* @param identity identity (ensures that the lock will not be released by anyone else)
* @return
* public boolean releaseLock(String lockKey, String identity) {
String luaScript = "if " +
" redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
Object result = redisTemplate.execute(redisScript, keys, identity);
return (boolean) result;
}
}
使用例
ここにはキーコードのみが掲載されていますが、ロックのキーはビジネスロジックに従って命名され、同じリクエストを一意に識別できることに注意してください。 valueにはUUIDを設定し、ロックを解放するときに正しく解放されるようにします(追加したロックのみが解放されます)。
@Autowired
private RedisLock redisLock; // redis Distributed lock
String redisLockKey = String.format("%s:docker-image:%s", REDIS_LOCK_PREFIX, imageVo.getImageRepository());
String redisLockValue = UUID.randomUUUID().toString();
try {
if (!redisLock.lock(redisLockKey, redisLockValue, REDIS_LOCK_TIMEOUT)) {
logger.info("redisLockKey [" + redisLockKey + "] already exists, not performing mirror insertion and update");
result.setMessage("New mirror frequently, retry later, lock occupied");
return result;
}
... // Execute the business logic
catch (Execpion e) {
... // Exception handling
} finally { // Release the lock
if (!redisLock.releaseLock(redisLockKey, redisLockValue)) {
logger.error("Free redis lock [" + redisLockKey + "] failed);
} else {
logger.error("Free redis lock [" + redisLockKey + "] success");
}
}
参考資料
https://www.jianshu.com/p
<未定義
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock
今回はSpringBootのRedis分散ロックによる並行処理問題の解決についてご紹介します。
関連
-
Redisのインクリメント呼び出しが失敗する理由と推奨される使い方を大きな白い嘘で解説
-
redisプラグインbloom-filterをcentosにインストールする方法
-
redisでluaスクリプトを使用するためのチュートリアル
-
SpringBootのRedis連携のアイデア解説
-
SpringBootプロジェクトにおけるRedis。包括的なアプリケーション
-
インタビューFAQです。Redisキャッシュとデータベース間のデータ整合性を確保する方法
-
Redisによる分散シングルナンバーと分散ID(カスタムルール生成)
-
redisを使ってnearly peopleの機能を実装する
-
Redisデータ永続化技術解説
-
Redisトランザクション処理の使用方法
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン