同時実行条件下でデータの一貫性を確保するためにロックを実装するにはどうすればよいですか? | JDクラウド技術チーム

モノリシックアーキテクチャ下でのロックの実装計画

1. ReentrantLock グローバル ロック

ReentrantLock (リエントラント ロック) は、スレッドが保持しているロックによって保護されている重要なリソースに再度アクセスしたときに成功する再入リクエストを指します。

一般的に使用されている同期と単純に比較してください。

  リエントラントロック 同期済み
ロック実装メカニズム AQSに依存する モニターモード
柔軟性 応答タイムアウト、中断、ロック取得の試行をサポート 柔軟性がない
リリースフォーム ロックを解放するには、明示的にunlock()を呼び出す必要があります。 オートリリースモニター
ロックタイプ 公平なロックと不公平なロック 不当なロック
条件付きキュー 複数の条件キューに関連付けることができます 条件付きキューを関連付ける
リエントランシー リエントラント リエントラント

AQS メカニズム: 要求された共有リソースがアイドル状態の場合、現在リソースを要求しているスレッドは有効な作業スレッドとして設定され、共有リソースはcompareAndSetStateCAS を通じてロック状態に設定されます。共有リソースが占有されている場合は、特定のブロッキングとロック割り当てを保証するために、待機ウェイクアップ メカニズム (CLH バリアントの FIFO デキュー) が使用されます。

再入可能: 公正なロックであっても不公平なロックであっても、ロック プロセスは状態値を使用します。

private volatile int state

  • 初期化時の状態値は 0 で、ロックを保持しているスレッドがないことを示します。
  • スレッドがロックを要求すると状態値が 1 増加し、同じスレッドが複数回ロックを取得すると状態値が複数回 1 増加する、これがリエントランシーの概念です。
  • ロックを解除すると、状態値が 0 になるまで 1 ずつ減分され、このスレッドはロックを解放します。
public class LockExample {

    static int count = 0;
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                try {
                    // 加锁
                    lock.lock();
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                finally {
                    // 解锁,放在finally子句中,保证锁的释放
                    lock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count: " + count);
    }
}

/**
 * 输出
 * count: 20000
 */

2. MySQL行ロック、オプティミスティックロック

楽観的ロックは、CAS の考え方に基づいて実装されるロックフリーの考え方であり、MySQL では、バージョン番号 + CAS ロックフリーの形式で実装されます。たとえば、2 つのトランザクション T1 と T2 が同時に実行される場合、 T2トランザクション実行時 サブミット成功後はバージョンが+1されるため、T1トランザクション実行のバージョン条件は成立しません。

SQL ステートメントとステート マシンをロックすると、異なるスレッドによるカウント値への同時アクセスによって引き起こされるデータの不整合を回避することもできます。

// 乐观锁 + 状态机
update
    table_name
set
    version = version + 1,
    count = count + 1
where
    id = id AND version = version AND count = [修改前的count值];

// 行锁 + 状态机
 update
    table_name
set
    count = count + 1
where
    id = id AND count = [修改前的count值]
for update;

3. きめ細かい ReetrantLock ロック

ReentrantLock を直接使用してグローバルにロックすると、この場合 1 つのスレッドがロックを取得し、プログラム全体のすべてのスレッドがここに来るとブロックされますが、プロジェクトでは操作中にユーザーごとに相互排他ロジックを実装したいと考えています。したがって、よりきめの細かいロックが必要です。

public class LockExample {
    private static Map<String, Lock> lockMap = new ConcurrentHashMap<>();
    
    public static void lock(String userId) {
        // Map中添加细粒度的锁资源
        lockMap.putIfAbsent(userId, new ReentrantLock());
        // 从容器中拿锁并实现加锁
        lockMap.get(userId).lock();
    }
    public static void unlock(String userId) {
        // 先从容器中拿锁,确保锁的存在
        Lock locak = lockMap.get(userId);
        // 释放锁
        lock.unlock();
    }
}

短所: 各ユーザーが共有リソースを要求すると、一度ロックされます。ユーザーはプラットフォームに再度ログインすることはありませんが、ロック オブジェクトは常にメモリ内に存在します。これはメモリ リークに相当するため、ロック タイムアウトは発生します。排除メカニズムのメカニズムを実装する必要がある。

4. きめ細かい同期グローバル ロック

上記のロック機構はロック コンテナ を使用していますConcurrentHashMapが、スレッドの安全性を確保するため、Synchronizedこの機構は依然として最下層で使用されているため、場合によっては、lockMap を使用する場合は 2 層のロックを追加する必要があります。

では、これを直接使用して、Synchronizedきめの細かいロック機構を実装できるでしょうか?

public class LockExample {
    public static void syncFunc1(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            // 模拟业务耗时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "释放锁了");
        }
    }

    public static void syncFunc2(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            // 模拟业务耗时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "释放锁了");
        }
    }

    // 使用 Synchronized 来实现更加细粒度的锁
    public static void main(String[] args) {
        new Thread(()-> syncFunc1(123456L), "Thread-1").start();
        new Thread(()-> syncFunc2(123456L), "Thread-2").start();
    }
}

/**
 * 打印
 * Thread-1拿到锁了
 * Thread-1释放锁了
 * Thread-2拿到锁了
 * Thread-2释放锁了
 */

  • コードから、ロックを実装するオブジェクトは、実際にはユーザー ID に関連する文字列オブジェクトであることがわかりました。ここで質問があるかもしれません。新しいスレッドが入るたびに、新しいスレッドは新しい文字列オブジェクトになりますが、文字列の場合は、コンテンツは同じですが、共有リソースを安全にロックするにはどうすればよいでしょうか?
  • これは実際には次の関数の機能によるものである必要がありますintern()
  • intern()この関数は、実行時にヒープ領域の文字列定数プールに文字列を追加するために使用されます。文字列が既に存在する場合は、文字列定数プールへの参照を返します。

分散アーキテクチャ下でのロックの実装計画

中心的な問題: このミューテックスを定義するには、複数のプロセス間のすべてのスレッドから見える領域を見つける必要があります。

優れた分散ロック実装ソリューションは、次の特性を満たしている必要があります。

  1. 分散環境では、異なるプロセス間のスレッドの相互排他を保証できます。
  2. 同時に、ロック リソースを正常に取得できるスレッドは 1 つだけです。
  3. ミューテックスが保証されている場合は、高可用性を確保する必要があります
  4. 高いパフォーマンスでロックを取得および解放できるようにするため
  5. 同じスレッドのロックの再入をサポートできます
  6. 合理的なブロック メカニズムを備え、ロックの競合に失敗したスレッドには対応するソリューションが必要です。
  7. ノンブロッキングロックの取得をサポートします。ロックの取得に失敗したスレッドは直接戻ることができます
  8. タイムアウト障害などの合理的なロック障害メカニズムを備えていれば、デッドロック状況を確実に回避できます。

Redis は分散ロックを実装します

  • Redis はミドルウェアであり、独立してデプロイできます。
  • これはさまざまな Java プロセスから認識され、パフォーマンスも非常に優れています。
  • redis 自体が提供する命令に依存してsetnx key value分散ロックを実装します。通常の命令との違いはset、キーが存在しない場合にのみ設定が成功し、キーが存在する場合は設定失敗が返されることです。

コード例:

// 扣库存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // 获取锁
    String lockKey = "lock-" + inventory.getInventoryId();
    int timeOut = 100;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "竹子-熊猫",timeOut,TimeUnit.SECONDS);
    // 加上过期时间,可以保证死锁也会在一定时间内释放锁
    stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
    
    if(!flag){
        // 非阻塞式实现
        return "服务器繁忙...请稍后重试!!!";
    }
    
    // ----只有获取锁成功才能执行下述的减库存业务----        
    try{
        // 查询库存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "库存不足,请联系卖家....";
        }
        
        // 扣减库存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
    } catch (Exception e) { // 确保业务出现异常也可以释放锁,避免死锁
        // 释放锁
        stringRedisTemplate.delete(lockKey);
    }
    
    if (n > 0)
        return "端口-" + port + ",库存扣减成功!!!";
    return "端口-" + port + ",库存扣减失败!!!";
}

作者:竹子爱熊猫
链接:https://juejin.cn/post/7038473714970656775

有効期限の合理的な分析:

なぜなら、事業者ごとに設定する有効期限の長さも異なり、長すぎる場合は不適切、短すぎる場合は不適切であるためです。

そこで私たちが考えた解決策は、現在のロック リソースの寿命を延ばすためにサブスレッドを設定することでした。具体的な実装では、サブスレッドは 2 ~ 3 秒ごとにキーの有効期限が切れているかどうかをチェックします。有効期限が切れていない場合は、ビジネス スレッドがまだビジネスを実行していることを意味し、キーの有効期限に 5 秒が追加されます。鍵。

ただし、メインスレッドが誤って死んでしまうことを防ぐために、サブスレッドがメインスレッドのために生き続ける「不死ロック」現象が発生するため、サブスレッドはメイン(業務)のデーモンスレッドとなります。サブスレッドがメインスレッドの後に続くようにスレッドを作成します。

// 续命子线程
public class GuardThread extends Thread { 
    private static boolean flag = true;

    public GuardThread(String lockKey, 
        int timeOut, StringRedisTemplate stringRedisTemplate){
        ……
    }

    @Override
    public void run() {
        // 开启循环续命
        while (flag){
            try {
                // 先休眠一半的时间
                Thread.sleep(timeOut / 2 * 1000);
            }catch (Exception e){
                e.printStackTrace();
            }
            // 时间过了一半之后再去续命
            // 先查看key是否过期
            Long expire = stringRedisTemplate.getExpire(
                lockKey, TimeUnit.SECONDS);
            // 如果过期了,代表主线程释放了锁
            if (expire <= 0){
                // 停止循环
                flag = false;
            }
            // 如果还未过期
            // 再为则续命一半的时间
            stringRedisTemplate.expire(lockKey,expire
                + timeOut/2,TimeUnit.SECONDS);
        }
    }
}


// 创建子线程为锁续命
GuardThread guardThread = new GuardThread(lockKey,timeOut,stringRedisTemplate);
// 设置为当前 业务线程 的守护线程
guardThread.setDaemon(true);
guardThread.start();

作者:竹子爱熊猫 
链接:https://juejin.cn/post/7038473714970656775

Redis マスター/スレーブ アーキテクチャでのロック失敗の問題

開発プロセス中に Redis の高可用性を確保するために、マスター/スレーブ レプリケーション アーキテクチャを使用して読み取りと書き込みを分離し、それによって Redis のスループットと可用性を向上させます。ただし、スレッドが Redis マスター ノードのロックの取得に成功した場合、スレーブ ノードにコピーする前にマスター ノードがクラッシュします。このとき、別のスレッドが Redis にアクセスしてスレーブ ノードにアクセスし、同時に別のスレッドが Redis にアクセスしてスレーブ ノードにアクセスします。ロックが正常に取得されました。このセキュリティの問題は、重要なリソースにアクセスするときに発生する可能性があります。

解決:

  • レッドロックアルゴリズム (公式ソリューション): 複数の独立した Redis が同時にデータを書き込みます。ロック有効期限内に、半数以上のマシンが書き込みに成功すると、ロックの取得成功が返されます。失敗した場合は、成功したマシンが返されます。ロックが解除されます。ただし、このアプローチの欠点は、コストが高く、複数の Redis ノードを個別にデプロイする必要があることです。
  • ロック ステータスの追加記録: 独自に展開された他のミドルウェア (DB など) を通じてロック ステータスを記録します。新しいスレッドがロックを取得する前に、DB 内のロック保持レコードをクエリする必要があります。ロック ステータスが保持されていない限り、 , 次に、分散ロックの取得を試みます。ただし、この状況のデメリットは明らかで、ロックを取得する処理の実装が難しく、パフォーマンスのオーバーヘッドも非常に高く、また、DB 内のロック状態を更新するにはタイマー機能と連携する必要があります。合理的なロック失敗メカニズムを確保します。
  • Zookepperを使用して実装

Zookeeper は分散ロックを実装します

Zookeeper データは Redis データとは異なります。データはリアルタイムで同期されます。マスター ノードが書き込みを行った後、正常に返されるまでにノードの半分以上が書き込む必要があります。したがって、電子商取引や教育などのプロジェクトで高いパフォーマンスを追求する場合は、ある程度の安定性をあきらめることができ、金融、銀行、政府などのプロジェクトで高いパフォーマンスを追求する場合は、redis を使用することをお勧めします。安定性が低下し、パフォーマンスの一部が犠牲になる可能性があるため、Zookeeper を使用して実装することをお勧めします。

分散ロックのパフォーマンスの最適化

上記のロックは確かに同時実行状況におけるスレッド セーフの問題を解決しますが、100 万人のユーザーが同時に 1,000 個の製品を購入しようと殺到するシナリオをどのように解決すればよいでしょうか?

  • 共有リソースを事前にウォームアップし、1 つのコピーをセグメントに保存できます。急ぎ購入の時間は午後15時ですが、事前に商品の数量を14時30分頃に10回に分け、同時実行異常を防ぐために各データを個別にロックします。
  • さらに、redis に 10 個のキーを書き込む必要があります。各新しいスレッドが入ってきてランダムにロックを割り当て、後続の在庫削減ロジックを実行します。完了後、ロックは後続のスレッドで使用できるように解放されます。
  • このような分散ロックの考え方は、本来1つのロックで実現できる共有リソースへのマルチスレッド同期アクセスの機能を実装し、瞬間的な条件下でのマルチスレッドのアクセス速度を向上させることです。同時性と安全性を確保するためにも必要です。

参考記事:

  1. https://juejin.cn/post/7236213437800890423

  2. https://juejin.cn/post/7038473714970656775

  3. https://tech.meituan.com/2019/12/05/aqs-理論-and-apply.html

著者: JD Technology の Jiao Zebin

出典:JD Cloud Developer Community 転載の際は出典を明記してください

IntelliJ IDEA 2023.3 と JetBrains Family Bucket の年次メジャー バージョン アップデート 新しいコンセプト「防御型プログラミング」: 安定した仕事に就く GitHub.com では 1,200 を超える MySQL ホストが稼働していますが、8.0 にシームレスにアップグレードするにはどうすればよいですか? Stephen Chow の Web3 チームは来月、独立したアプリをリリースする予定ですが、 Firefox は廃止されるのでしょうか? Visual Studio Code 1.85 リリース、フローティング ウィンドウ Yu Chengdong: ファーウェイは来年破壊的な製品を発売し、業界の歴史を書き換えるだろう 米国 CISA はメモリ セキュリティの脆弱性を排除するために C/C++ の廃止を勧告 TIOBE 12 月: C# がプログラミングになると予想30年前 雷軍が書いた論文「コンピュータウイルス判定エキスパートシステムの原理と設計」
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10319213