[[323418]] 導入 最近、プロジェクトが非常に頻繁に立ち上げられています。私は何日も続けて残業し、夜更かしをしています。体力的にも疲れていて、精神的にもだるさを感じます。しかし、ビジネス面から厳しいプレッシャーがかかっており、締め切りが迫っているため、覚悟を決めて実行しなければなりません。頭がぼんやりしているときに書いたものはコードとは言えず、直接的にバグと呼ぶことができます。バグを修正するために夜更かししたら、ひどく叱られました。 当社はショッピングモール事業を営んでいるため、商品の在庫を頻繁に減額する必要があります。アプリケーションはクラスターにデプロイされます。同時実行性や、在庫の買いすぎや売りすぎなどの問題を回避するために、Redis 分散ロックを使用して制御します。在庫減算コードに lock.tryLock を追加すれば問題は解決すると思いました。 - /**
- * @著者 xiaofu
- * @description 在庫の減額
- * @日付2020/4/21 12:10
- */
- パブリック文字列stockLock() {
- RLock ロック = redissonClient.getLock( "stockLock" );
- 試す {
- /**
- * ロックを取得する
- */
- ロックの試行回数が10回の場合、TimeUnit.SECONDSは次のように計算されます。
- /**
- * 在庫数を確認する
- */
- 整数在庫 = Integer .valueOf(stringRedisTemplate.opsForValue().get( "stockCount" ));
- /**
- * 在庫を差し引く
- */
- (在庫>0)の場合{
- 在庫 = 在庫 - 1;
- stringRedisTemplate.opsForValue() です。 ( "stockCount" 、 stock.toString()を設定します);
- LOGGER.info( "在庫減算が成功しました。残りの在庫数量: {}" , stock);
- }それ以外{
- LOGGER.info( "在庫が足りません~" );
- }
- }それ以外{
- LOGGER.info( "ロックを取得できませんでした。処理は終了しました。" );
- }
- } キャッチ (例外 e) {
- LOGGER.info( "例外処理" , e);
- ついに
- ロックを解除します。
- }
- 戻る "わかりました" ;
- }
その結果、業務コードの実行後にロック lock.unlock() を解除し忘れ、Redis スレッド プールがいっぱいになり、Redis サービスが大規模に失敗しました。このため在庫データの控除に混乱が生じ、上司から叱責を受けました。今月の公演〜あ〜。 Redis ロックを長期間使用していくうちに、Redis ロックには想像していたよりもはるかに多くの落とし穴があることがわかりました。面接の質問でも、Redis の分散ロックは頻繁に登場します。たとえば、「ロックの使用時にどのような問題が発生しましたか?」、「どのように解決しましたか?」これらは基本的に一連の質問です。 今日は、Redis 分散ロックの使用経験といくつかの解決策を皆さんと共有したいと思います。 1. ロックが解除されない これは低レベルのエラーであり、上で私が犯した間違いです。現在のスレッドが Redis ロックを取得し、ビジネス処理後に時間内にロックを解放しないため、他のスレッドはロックの取得を試行し続け、ブロックされます。たとえば、Jedisクライアントを使用すると、次のエラーメッセージが報告されます。 - redis.clients.jedis.exceptions.JedisConnectionException:プールからリソースを取得できませんでした
Redis スレッド プールには、クライアント コマンドを処理するアイドル スレッドがありません。 解決策も非常に簡単です。注意していれば、ロックを取得したスレッドは、業務処理が終わったらすぐにロックを解除します。ロックが再度入力されてもロックが取得されない場合、スレッドは現在の接続を解放し、しばらくスリープすることができます。 - パブリックボイドロック(){
- (真)の間{
- ブールフラグ = this.getLock(キー);
- if (フラグ) {
- やるべきこと.........
- }それ以外{
- // 現在のRedis接続を解放する
- redis.close ();
- // 1000ミリ秒間スリープする
- スリープ(1000);
- }
- }
- }
2. BのロックがAによって解除される Redis ロック実装の原理は SETNX コマンドにあることがわかっています。キーが存在しない場合は、キーの値が値に設定され、戻り値は 1 になります。指定されたキーがすでに存在する場合、SETNX は何もアクションを実行せず、戻り値は 0 になります。 - SETNXキー値
次のシナリオを想像してください: 2 つのスレッド A と B がキー myLock をロックしようとします。スレッド A が最初にロックを取得し (ロックが 3 秒後に期限切れになった場合)、スレッド B はロックの取得を待機します。これには何も問題はありません。 このとき、ビジネスロジックに時間がかかり、実行時間が Redis ロックの有効期限を超えている場合は、スレッド A のロックが自動的に解除され (キーが削除され)、スレッド B はキー myLock が存在しないことを検出し、SETNX コマンドを実行してロックを取得します。 ただし、スレッド A はビジネス ロジックの実行を終了した後もロックを解除 (キーを削除) するため、スレッド B のロックがスレッド A によって解除されます。 上記の状況を回避するには、通常、ロック時に各スレッドを識別するための独自の一意の値を持ち込み、指定された値のキーのみを解放する必要があります。そうしないと、混乱したロック解放シナリオが発生します。 3. データベーストランザクションのタイムアウト えーっと〜Redis ロックについて話しているときに、なぜデータベース トランザクションについて話しているのでしょうか?急いで読み進めないで、次のコードを見てください。 - @トランザクション
- パブリックボイドロック(){
-
- (真)の間{
- ブールフラグ = this.getLock(キー);
- if (フラグ) {
- 入れる();
- }
- }
- }
トランザクションを開始するには、このメソッドに @Transaction アノテーションを追加します。コード内で例外がスローされた場合は、ロールバックします。データベース トランザクションにはタイムアウト制限があり、時間のかかるデータベース操作を無条件に待機するわけではないことに注意してください。 たとえば、大きなファイルを解析し、そのデータをデータベースに保存します。実行時間が長すぎる場合、トランザクションはタイムアウトになり、自動的にロールバックされます。 キーを長時間ロックできず、ロック取得の待機時間がデータベース トランザクション タイムアウトを大幅に超えると、プログラムは例外を報告します。 通常、この問題を解決するには、データベース トランザクションを手動のコミットおよびロールバック トランザクションに変更する必要があります。 - オートワイヤード
- データソーストランザクションマネージャーデータソーストランザクションマネージャー;
-
- @取引
- パブリックボイドロック(){
- //トランザクションを手動で開始する
- トランザクションステータス transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
- 試す {
- (真)の間{
- ブールフラグ = this.getLock(キー);
- if (フラグ) {
- 入れる();
- //トランザクションを手動でコミットする
- dataSourceTransactionManager.commit (トランザクションステータス) ;
- }
- }
- } キャッチ (例外 e) {
- //トランザクションを手動でロールバックする
- dataSourceTransactionManager.rollback (トランザクションステータス);
- }
- }
4. ロックの有効期限が切れたが、業務が完了していない この状況は上で述べた 2 番目の状況と似ていますが、解決策は少し異なります。 同じシナリオでは、Redis 分散ロックの有効期限が切れますが、ビジネス ロジックは実行されていません。しかし、ここで考え方を変えて問題について考えてみましょう。 redis ロックの有効期限を延長すれば解決するのではないでしょうか? まだ問題が残っています。ロック時にRedisロックの有効期限を手動で延長することはできますが、適切な時間はどれくらいでしょうか?ビジネスロジックの実行時間は制御不可能であり、調整が長すぎると運用パフォーマンスに影響します。 redis ロックの有効期限が自動的に更新されると便利です。 この問題を解決するには、redis クライアント redisson を使用します。 Redisson は、分散環境における Redis のいくつかの難しい問題を解決します。その目的は、ユーザーが Redis に注意を払う必要が少なくなり、ビジネス ロジックの処理に多くの労力を費やすことができるようにすることです。 Redisson は分散ロックのカプセル化をうまく行っており、API を呼び出すだけで済みます。 - RLock ロック = redissonClient.getLock( "stockLock" );
ロックが正常に完了すると、redisson はロックを監視し、10 秒ごとにロックをチェックするスケジュールされたタスクを登録します。ロックがまだ保持されている場合は、有効期限が更新されます。デフォルトの有効期限は 30 秒です。このメカニズムは「ウォッチドッグ」とも呼ばれます。 。 。 たとえば、ロック時間が 30 秒の場合、10 秒ごとにチェックされます。ロックされた業務が完了しない場合は更新され、ロックの有効期限は 30 秒にリセットされます。 次の redisson ソース コード実装を分析すると、契約のロック、ロック解除、更新に関係なく、クライアントは複雑なビジネス ロジックを Lua スクリプトにカプセル化して redis に送信し、この複雑なビジネス ロジックの実行のアトミック性を確保していることがわかります。 - 翻訳者
- @サービス
- パブリッククラスRedisDistributionLockPlus{
-
- /**
- * ロック タイムアウト (ミリ秒単位)。つまり、操作はロック時間内に実行されます。完了しない場合は、同時実行現象が発生します。
- */
- プライベート静的最終長い DEFAULT_LOCK_TIMEOUT = 30;
-
- プライベート静的最終long TIME_SECONDS_FIVE = 5;
-
- /**
- * 各キーの有効期限{@link LockContent}
- */
- プライベート Map<String, LockContent> lockContentMap = 新しい ConcurrentHashMap<>(512);
-
- /**
- * Redisは実行成功を返します
- */
- プライベート静的最終Long EXEC_SUCCESS = 1L;
-
- /**
- * ロック lua スクリプトを取得します。k1: ロックキー、k2: 更新時間キー、arg1: requestId、arg2: タイムアウト時間
- */
- プライベート静的最終文字列LOCK_SCRIPT = "redis.call('exists', KEYS[2]) == 1の場合、ARGV[2] = math.floor(redis.call('get', KEYS[2]) + 10)終了" +
- 「redis.call('exists', KEYS[1]) == 0の場合」 +
- "ローカル t = redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2]) " +
- 「k、v のペア(t) に対して、次の操作を実行します」 +
- 「v == 'OK' の場合、数値(ARGV[2]) を返します。終了」 +
- 「終了」 +
- "0を返す終了" ;
-
- /**
- * ロック解除 lua スクリプト、k1: ロック取得キー、k2: 更新時間キー、arg1: requestId、arg2: ビジネス時間、arg3: ビジネス開始時に設定したタイムアウト
- */
- プライベート静的最終文字列UNLOCK_SCRIPT = "redis.call('get', KEYS[1]) == ARGV[1]の場合、" +
- "ローカル ctime = tonumber(ARGV[2]) " +
- "ローカルbiz_timeout = tonumber(ARGV[3]) " +
- 「ctime > 0 の場合」 +
- 「redis.call('exists', KEYS[2]) == 1の場合」 +
- "ローカルavg_time = redis.call('get', KEYS[2]) " +
- 「avg_time = (tonumber(avg_time) * 8 + ctime * 2)/10」 +
- 「avg_time >= biz_timeout - 5 の場合、redis.call('set', KEYS[2], avg_time, 'EX', 24*60*60)」 +
- "そうでない場合は redis.call('del', KEYS[2]) 終了 " +
- "elseif ctime > biz_timeout -5 then redis.call('set', KEYS[2], ARGV[2], 'EX', 24*60*60) 終了 " +
- 「終了」 +
- "redis.call('del', KEYS[1]) を返す " +
- "そうでなければ0を返す終了" ;
- /**
- * 更新luaスクリプト
- */
- private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end" ;
-
-
- プライベート最終 StringRedisTemplate redisTemplate;
-
- パブリックRedisDistributionLockPlus(StringRedisTemplate redisTemplate) {
- redisテンプレートをコピーします。
- ScheduleTask タスク = 新しい ScheduleTask(this、lockContentMap);
- // スケジュールされたタスクを開始する
- ScheduleExecutor.schedule(タスク、1、1、TimeUnit.SECONDS);
- }
-
- /**
- * ロック
- * ロックを取得した場合は、ロックします。ロックが取得されない場合は、ロックが取得されるまで待機し続けます。
- *
- * @param ロックキー
- * @param requestId グローバルに一意
- * @param expire ロックの有効期限(秒単位)
- * @戻る
- */
- パブリックブールロック(文字列ロックキー、文字列リクエストID、長い有効期限) {
- log.info( "ロックを開始します。lockKey ={}、requestId={}" 、lockKey、requestId);
- のために(; ; ) {
- // Redis への負荷を軽減するために、ロックを保持しているスレッドがすでに存在するかどうかを判断します。
- ロックコンテンツ lockContentOld = lockContentMap.get(lockKey);
- ブール値 unLocked = null == lockContentOld;
- // ロックされていない場合はロックを取得する
- if (ロック解除) {
- 長い開始時間 = System.currentTimeMillis();
- // タイムアウトを計算する
- 長いbizExpire = 有効期限 == 0L ? DEFAULT_LOCK_TIMEOUT: 期限切れ;
- 文字列 lockKeyRenew = lockKey + "_renew" ;
-
- RedisScript<Long> スクリプト = RedisScript。 (LOCK_SCRIPT、Long.class)の;
- リスト<String>キー = 新しいArrayList<>();
- キーを追加します(lockKey);
- キーを追加します(lockKeyRenew);
- 長い lockExpire = redisTemplate。スクリプト、キー、リクエストID、Long.toString(bizExpire)を実行します。
- ( null != lockExpire && lockExpire > 0)の場合{
- //ロックをマップに配置する
- LockContent を新しい LockContent() に追加します。
- lockContent.setStartTime(startTime);
- ロックコンテンツにロック期限を設定します(ロック期限);
- lockContent.setExpireTime(開始時間 + lockExpire * 1000);
- ロックコンテンツ。リクエストIDを設定します。
- スレッドをロックします。
- ロックコンテンツ。BizExpire を設定します。
- ロックコンテンツ.setLockCount(1);
- lockContentMap に lockKey を追加します。
- log.info( "正常にロックされました、lockKey ={}、requestId={}" 、lockKey、requestId);
- 戻る 真実;
- }
- }
- // ロックを繰り返し取得します。スレッド プール内のスレッドの再利用により、スレッドの等価性では、それがこのスレッドのロックであるかどうかを判断できません。
- (Thread.currentThread() == lockContentOld.getThread() の場合
- && リクエストID.equals(lockContentOld.getRequestId())){
- // カウント +1
- lockContentOld.setLockCount(lockContentOld.getLockCount()+1);
- 戻る 真実;
- }
-
- // ロックされている場合、またはロックの取得に失敗した場合は、100ミリ秒待機します
- 試す {
- TimeUnit.MILLISECONDS.sleep(100);
- } キャッチ (InterruptedException e) {
- // ここでlombokを使用すると問題があります
- log.error( "Redis ロックの取得に失敗しました。lockKey ={}、requestId={}" 、lockKey、requestId、e);
- 戻る 間違い;
- }
- }
- }
-
-
- /**
- * ロック解除
- *
- * @param ロックキー
- * @param ロック値
- */
- パブリックブール値のロック解除(文字列ロックキー、文字列ロック値) {
- 文字列 lockKeyRenew = lockKey + "_renew" ;
- ロックコンテンツ lockContent = lockContentMap.get(lockKey);
-
- 消費時間が長い;
- ロックコンテンツがnull の場合
- 消費時間 = 0L;
- }それ以外の場合 (lockValue.equals(lockContent.getRequestId())) {
- ロックカウントをロックコンテンツに追加します。
- // ロックが解除されるたびにカウントは -1 になり、0 になると Redis 上のキーが削除されます
- (
- ロックコンテンツを設定します。
- 戻る 間違い;
- }
- 消費時間 = (System.currentTimeMillis() - lockContent.getStartTime()) / 1000;
- }それ以外{
- log.info( "ロックの解放に失敗しました。これはあなた自身のロックではありません。" );
- 戻る 間違い;
- }
-
- // 完了したキーを削除し、Redis の負荷を軽減するためにまずローカル キャッシュを削除します。分散ロックは 1 つしかないため、ここではロックは追加されません。
- ロックコンテンツマップを削除します。
-
- RedisScript<Long> スクリプト = RedisScript。 (UNLOCK_SCRIPT、Long.class)の;
- リスト<String>キー = 新しいArrayList<>();
- キーを追加します(lockKey);
- キーを追加します(lockKeyRenew);
-
- 長い結果 = redisTemplate。実行(スクリプト、キー、lockValue、Long.toString(consumeTime)、
- ロックコンテンツを取得する
- EXEC_SUCCESS.equals(結果)を返します。
-
- }
-
- /**
- * リニューアル
- *
- * @param ロックキー
- * @param ロックコンテンツ
- * @戻る true : 更新は成功、 false : 更新は失敗 (1. 更新期間中に実行が完了し、ロックが解除された、2. 自分のロックではない、3. 更新期間中にロックが期限切れになった (未解決))
- */
- パブリックブール値更新(文字列ロックキー、ロックコンテンツロックコンテンツ) {
-
- // 実行ビジネススレッドのステータスを確認する
- スレッドの状態 = lockContent.getThread().getState();
- if (Thread.State.TERMINATED == 状態) {
- log.info( "ビジネスを実行しているスレッドは終了しており、更新されません lockKey ={}, lockContent={}" , lockKey, lockContent);
- 戻る 間違い;
- }
-
- 文字列 requestId = lockContent.getRequestId();
- 長いtimeOut = (lockContent.getExpireTime() - lockContent.getStartTime()) / 1000;
-
- RedisScript<Long> スクリプト = RedisScript。 (RENEW_SCRIPT、Long.class)の;
- リスト<String>キー = 新しいArrayList<>();
- キーを追加します(lockKey);
-
- 長い結果 = redisTemplate。スクリプト、キー、リクエストID、Long.toString(timeOut)を実行します。
- log.info( "更新結果、成功の場合は True、失敗の場合は False lockKey ={}、result={}" 、 lockKey、 EXEC_SUCCESS.equals(result));
- EXEC_SUCCESS.equals(結果)を返します。
- }
-
-
- 静的クラス ScheduleExecutor {
-
- 公共 静的void スケジュール(ScheduleTask タスク、long initialDelay、long 期間、TimeUnit 単位) {
- 長い遅延 = unit.toMillis(initialDelay);
- 長い期間_ = 単位.toMillis(期間);
- // スケジュールされた実行
- 新しいタイマー( "Lock-Renew-Task" ).schedule(task, delay, period_);
- }
- }
-
- 静的クラス ScheduleTask は TimerTask を拡張します {
-
- プライベート最終 RedisDistributionLockPlus redisDistributionLock;
- プライベート最終 Map<String, LockContent> lockContentMap;
-
- パブリックScheduleTask(RedisDistributionLockPlus redisDistributionLock、Map<String、LockContent> lockContentMap) {
- redisDistributionLock をオーバーライドします。
- ロックコンテンツマップをロックします。
- }
-
- @オーバーライド
- パブリックボイド実行(){
- lockContentMap.isEmpty() の場合 {
- 戻る;
- }
- <Map.Entry<String, LockContent>>を設定します。entries = lockContentMap.entrySet();
- Map.Entry <String, LockContent> エントリ:エントリ) {
- 文字列 lockKey = entry.getKey();
- ロックコンテンツ lockContent = entry.getValue();
- 長い有効期限 = lockContent.getExpireTime();
- // スレッドプール内のタスク数を減らす
- if ((expireTime - System.currentTimeMillis())/ 1000 < TIME_SECONDS_FIVE) {
- //スレッドプールの非同期更新
- スレッドプール.submit(() -> {
- ブール値の更新 = redisDistributionLock.renew(lockKey, lockContent);
- (更新)の場合{
- 長い expireTimeNew = lockContent.getStartTime() + (expireTime - lockContent.getStartTime()) * 2 - TIME_SECONDS_FIVE * 1000;
- ロックコンテンツ。有効期限を設定します(expireTimeNew);
- }それ以外{
- // 更新に失敗しました。実行が完了したか、 Redis に問題があることを示しています。
- ロックコンテンツマップを削除します。
- }
- });
- }
- }
- }
- }
- }
5. Redis マスタースレーブレプリケーションの落とし穴 Redis の高可用性を実現するための最も一般的なソリューションはマスター/スレーブ レプリケーションですが、これも Redis 分散ロックの問題を引き起こします。 Redis クラスター環境では、クライアント A が今すぐロックしたい場合、ルーティング ルールに従ってマスター ノードを選択し、キー mylock を書き込みます。ロックが成功すると、マスター ノードはキーを対応するスレーブ ノードに非同期的にコピーします。 この時点で Redis マスター ノードがダウンした場合、クラスターの可用性を確保するために、マスターとスレーブの切り替えが実行され、スレーブが Redis マスターになります。クライアント B は新しいマスター ノードを正常にロックし、クライアント A も正常にロックされたと認識します。 これにより、複数のクライアントが同時に分散ロックをロックすることになり、さまざまなダーティ データが生成されます。 解決策としては、現時点では治療法はありません。私たちにできるのは、機械の安定性を確保し、この事故が発生する可能性を減らすために最善を尽くすことだけです。 要約する 上記は、Redis 分散ロックを使用する際に遭遇した落とし穴の一部です。少し悲しい気がします。私はよく、この穴を埋めるために 1 つの方法を使用しますが、すぐに別の穴が出てくることに気づきます。実際のところ、完璧な解決策というものは存在しません。特効薬はありません。それは、長所と短所を比較検討した上で受け入れられる妥協案にすぎません。 |