Redisの分散ロックの不適切な使用は大きな事故を引き起こし、100本のFeitianMoutaiを売り過ぎました!!!

Redisに基づく分散ロックの使用は、最近では目新しいことではありません。

この記事は主に、実際のプロジェクトでのredis分散ロックによって引き起こされた事故の分析と解決策に基づいています。私たちのプロジェクトのスナップアップ注文は分散ロックによって解決されます。かつて、オペレーションはFeitian Moutaiのスナップアップキャンペーンを実行しました。100本のボトルが在庫にありましたが、100本が売られ過ぎでした!ご存知のように、この地球上で茅台酒を飛ばすことの不足!

事故はP0重大事故に分類されます...率直にしか受け入れられません。プロジェクトチーム全体のパフォーマンスが差し引かれました~~事故の後、CTOは私に名前を付けて、それに対処するために主導権を握るように頼みました。

さて、急いで〜

事故現場

ある程度理解した後、このパニック買いの活動インターフェースはこれまでに一度も起こったことがないことを知りましたが、なぜ今回は売られ過ぎなのですか?
その理由は、これまでのラッシュ購入商品は希少商品ではなかったのですが、今回は実はフェイティアン茅台酒でした。埋没地点データを分析することで、基本的に2倍のデータになり、イベントの熱気が想像できます!言うまでもありませんが、コアコードに直接アクセスすると、機密部分は擬似コードで処理されます。

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
    
    
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId;
    try {
    
    
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
        if (lockFlag) {
    
    
            // HTTP请求用户服务进行用户相关的校验
            // 用户活动校验
            
            // 库存校验
            Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
            assert stock != null;
            if (Integer.parseInt(stock.toString()) <= 0) {
    
    
                // 业务异常
            } else {
    
    
                redisTemplate.opsForHash().increment(key+":info", "stock", -1);
                // 生成订单
                // 发布订单创建成功事件
                // 构建响应VO
            }
        }
    } finally {
    
    
        // 释放锁
        stringRedisTemplate.delete("key");
        // 构建响应VO
    }
    return response;
}

上記のコードは、有効期間が10秒の分散ロックの有効期限までビジネスロジックに十分な実行時間を確保します。try-finallyステートメントブロックを使用して、ロックが時間内に解放されるようにします。在庫もビジネスコード内で検証されます。とても安全そうです〜心配しないで、分析を続けてください。

大手企業の面接資料がもっと必要な場合は、クリックして直接入力して無料で入手することもできます!パスワード:CSDN

事故原因

Feitian Maotaiのスナップアップ活動は、多くの新規ユーザーを引き付け、APPをダウンロードして登録しました。その中には、専門的な方法を使用して新規ユーザーを登録し、羊毛とブラシの注文を収集する羊毛パーティーがたくさんあります。もちろん、当社のユーザーシステムは事前に準備されており、Alibaba Cloudのヒューマンマシン検証、3要素認証、自社開発のリスク管理システム、その他18の武道へのアクセスにより、多数の違法ユーザーがブロックされています。ここが好きになります〜でも、そのせいでユーザーサービスの運用負荷が高くなっています。

パニック買いの活動が始まった瞬間、多数のユーザー確認要求がユーザーサービスにヒットしました。ユーザーサービスゲートウェイの応答遅延は短いです。一部のリクエストの応答時間は10秒を超えますが、HTTPリクエストの応答タイムアウトが30秒に設定されているため、ユーザー検証のためにインターフェイスがブロックされます。10秒後、ロックはこの時点で、新しいリクエストはロックを取得できます。これは、ロックが上書きされることを意味します。これらのブロックされたインターフェースが実行された後、ロックを解放するロジックが実行され、他のスレッドのロックが解放され、新しい要求がロックを奪い合う原因になります。これは実際には非常に悪いサイクルです。現時点では、在庫検証のみに頼ることができますが、在庫検証は非アトミックではありません。取得と比較の方法が使用されます。売られ過ぎの悲劇はこのように起こりました~~~

事故分析

注意深く分析した結果、このスナップアップインターフェイスには、主に次の3つの場所に集中している同時実行性の高いシナリオで重大なセキュリティリスクがあることがわかりました。

  • 他にシステムリスクのフォールトトレラントな処理はありません。
    ユーザーサービスが緊密であるため、ゲートウェイの応答は遅れますが、それに対処する方法はありません。これはオーバーセルのヒューズです。
  • 一見安全に見える分散ロックは、実際にはまったく安全ではありません。
    キー値を設定する方法[EX秒] [PXミリ秒] [NX | XX]を採用していますが、スレッドAが解放される前に長時間実行すると、ロックは期限切れになります。この時点で、スレッドBはロックを取得できます。スレッドAの実行が終了すると、ロックを解除すると、実際にはスレッドBのロックが解除されます。このとき、スレッドCは再びロックを取得できます。このとき、スレッドBがロック解除の実行を終了すると、実際には解放されたスレッドCによって設定されたロックになります。これが売られ過ぎの直接の原因です。
  • 非アトミックインベントリ検証
    非アトミックインベントリ検証は、同時シナリオで不正確なインベントリ検証結果につながります。これが売られ過ぎの根本的な原因です。

上記の分析によると、問題の根本的な原因は、在庫検証が分散ロックに大きく依存していることです。分散ロックの通常のセットとデルの場合、在庫検証に問題はありません。ただし、分散ロックが安全で信頼できない場合、インベントリの検証は役に立ちません。

解決

理由がわかれば、正しい薬を処方することができます。

比較的安全な分散ロックを実現する

比較的安全な定義:setとdelは1つずつマップされ、他に既存のロックdelはありません。実情の観点からは、セットとデルの1対1のマッピングを実現できたとしても、ビジネスの絶対的なセキュリティを保証することはできません。ロックの有効期限は常に制限されているため、有効期限が設定されていないか、有効期限が非常に長く設定されていない限り、これは他の問題も引き起こします。したがって、それは意味がありません。比較的安全な分散ロックを実現するには、キーの値に依存する必要があります。ロックが解除されると、値の一意性が使用されて、値が削除されないようにします。次のように、LUAスクリプトに基づいてアトミックgetとcompareを実装します。

public void safedUnLock(String key, String val) {
    
    
    String luaScript = "local in = ARGV[1] local curr=redis.call('get', KEYS[1]) if in==curr then redis.call('del', KEYS[1]) end return 'OK'"";
    RedisScript<String> redisScript = RedisScript.of(luaScript);
    redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));
}

LUAスクリプトを使用して安全にロックを解除します。

安全な在庫検証を実現する

並行性についてより深く理解している場合、get、compare / read、saveなどの操作はすべて非アトミックであることがわかります。アトミック性を実現したい場合は、LUAスクリプトを使用してそれを実現することもできます。ただし、この例では、パニック買いのアクティビティに入れることができるボトルは1つだけなので、LUAスクリプトの実装ではなく、redisのアトミック性に基づくことができます。その理由は:

// redis会返回操作之后的结果,这个过程是原子性的
Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

いいえ、コードの在庫チェックは完全に「不要」です。

改善されたコード

上記の分析の後、分散ロックを処理するための新しいDistributedLockerクラスを作成することにしました。

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
    
    
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId();
    String val = UUID.randomUUID().toString();
    try {
    
    
        Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
        if (!lockFlag) {
    
    
            // 业务异常
        }

        // 用户活动校验
        // 库存校验,基于redis本身的原子性来保证
        Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
        if (currStock < 0) {
    
     // 说明库存已经扣减完了。
            // 业务异常。
            log.error("[抢购下单] 无库存");
        } else {
    
    
            // 生成订单
            // 发布订单创建成功事件
            // 构建响应
        }
    } finally {
    
    
        distributedLocker.safedUnLock(key, val);
        // 构建响应
    }
    return response;
}

深い思考

分散ロックは必要ですか?

改善後、redis自体のアトミック控除の助けを借りて売られ過ぎないことを保証できることが実際にわかります。正しい。ただし、そのようなロックがない場合、すべての要求はビジネスロジックを通過します。他のシステムに依存しているため、この時点で他のシステムへのプレッシャーが高まります。これにより、パフォーマンスの低下とサービスの不安定性が増大し、その増加は損失の価値がありません。分散ロックに基づいて、一部のトラフィックはある程度傍受される可能性があります。

大手企業の面接資料がもっと必要な場合は、クリックして直接入力して無料で入手することもできます!パスワード:CSDN

分散ロックの選択

誰かがRedLockを使用して分散ロックを実装することを提案しました。RedLockの方が信頼性が高くなりますが、特定のパフォーマンスが犠牲になります。このシナリオでは、この信頼性の向上は、パフォーマンスの向上によってもたらされる費用対効果よりもはるかに劣っています。信頼性要件が非常に高いシナリオでは、RedLockを使用してそれを実現できます。

分散ロックについてもう一度考える必要がありますか?

バグは早急に修復する必要があるため、最適化してテスト環境でストレステストを実行し、すぐにオンラインで展開しました。この最適化は成功し、パフォーマンスはわずかに向上していることがわかります。分散ロック障害の場合、売られ過ぎの状況はありません。しかし、最適化の余地はありますか?いくつか!サービスはクラスターにデプロイされるため、インベントリをクラスター内の各サーバーに均等に分散し、クラスター内の各サーバーにブロードキャストで通知できます。ゲートウェイ層は、ユーザーIDに基づくハッシュアルゴリズムを使用して、要求するサーバーを決定します。このようにして、アプリケーションキャッシュに基づいて在庫の控除と判断を実現できます。パフォーマンスがさらに向上しました!

// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();

...

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
    
    
SeckillActivityRequestVO response;

    Long seckillId = request.getSeckillId();
    if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
    
    
        // 业务异常
    }
     // 用户活动校验
     // 库存校验
    if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
    
    
        SECKILL_FLAG_MAP.put(seckillId, false);
        // 业务异常
    }
    // 生成订单
    // 发布订单创建成功事件
    // 构建响应
    return response;
}

上記の変換により、redisにまったく依存する必要がなくなります。性能と安全性の両方をさらに向上させることができます!もちろん、このソリューションでは、マシンの動的な拡張や縮小などの複雑なシナリオは考慮されていません。これらを引き続き検討する場合は、分散ロックのソリューションを直接検討することをお勧めします。

総括する

希少な商品の売られ過ぎは間違いなく大きな事故です。売られ過ぎの量が多い場合、それはプラットフォームに非常に深刻な運用上および社会的影響を与えることさえあります。この事故の後、私はプロジェクトのコード行を軽視してはならないことに気づきました。そうしないと、シナリオによっては、これらの正常に機能するコードが致命的な殺人者になるでしょう!開発者にとって、開発計画を設計するときは、計画を徹底的に検討する必要があります。計画はどのように包括的に検討できますか?継続学習のみ!

読者のメリット

ここを見てくれてありがとう!
以下に示すように、2020年の最新のJavaインタビューの質問(回答を含む)とJava研究ノートをここにまとめました。
ここに画像の説明を挿入します

上記の面接の質問に対する回答は、ドキュメントノートにまとめられています。インタビューだけでなく、いくつかのメーカーに関する情報をまとめ、Zhentiの最新の2020コレクション(両方ともスクリーンショットのごく一部を文書化)を誰でも無料で共有できます。必要な場合は、クリックしてシグナルを入力できます:CSDN!自由に共有できます〜

この記事が気に入ったら、転送して気に入ってください。

私に従うことを忘れないでください!
ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/weixin_49527334/article/details/111858176