分散-分散ロックのシーンと実装

7386 ワード

完全なコースを学習するには、インターネットJavaフルスタックエンジニアに移行してください.
シーンの操作
まず、お客様が注文するときに、在庫センターを呼び出して在庫を減らすシーンを見てみましょう.一般的な操作は次のとおりです.
update store set num = $num where id = $id

このような在庫を設定する修正方式により、同時量が高い場合にデータベースの紛失更新があることがわかります.例えば、a,bの現在の2つの事務では、照会された在庫はすべて5で、aが3つのリストを買った場合に在庫を2に設定し、bが1つのリストを買った場合に在庫を4に設定すると、aがbを上書きする更新が発生します.より多くの条件を追加します
update store set num = $num where id = $id and num = $query_num

すなわち、楽観ロックの方式で処理することは、もちろんバージョン番号で楽観ロックを処理することもできますが、同じですが、これは1つのテーブルを更新します.もし私たちが複数のテーブルに関わっていたら、私たちはこのリストに関連するすべてのテーブルと同じ時間に1つのスレッドでしか更新できないことを望んでいます.複数のスレッドは異なる順序で同じリストに関連する異なるデータを更新します.デッドロックが発生する確率が高い.非敏感なデータに対して、私たちも楽観的なロック処理を加える必要はありません.私たちのサービスはすべてマルチマシンが配置しています.マルチプロセスマルチスレッドが同時に1つのプロセスの1つのスレッドしか処理できないことを保証するには、分散ロックを使用する必要があります.分散ロックの実装方法はたくさんありますが、今日はデータベース、Zookeeper、Redis、Tairの実装ロジックを介しています.
データベース実装
xxロックをかける
単子関連のすべてのデータを更新し、まずこの単子をクエリーし、排他ロックを加えて、一連の更新操作を行います.
begin transaction;
select ...for update;
doSomething();
commit();

この処理は主に排他ロックによって他のスレッドをブロックするが、これはいくつかの点に注意しなければならない.
  • クエリーのデータは必ずデータベースに存在しなければならない.存在しなければ、データベースにはgapロックが付加され、gapロックの間には互換性があり、このような2つのスレッドにはgapロックが付加され、もう1つが再更新されるとデッドロックが発生する.しかし、一般的に更新できるデータは
  • 存在します.
  • 以降の処理プロセスはできるだけ短い時間が必要である.すなわち、更新時にデータを事前に準備し、トランザクションの処理時間が十分に短く、プロセスが十分に短いことを保証する.オープントランザクションはずっと接続を占めているため、プロセスが長いとデータベース接続の
  • を消費しすぎる.
    ユニークキー
    1つのテーブルに一意のキーを作成することで、saveStoreを実行するなどのロックを取得します.
    insert table lock_store ('method_name') values($method_name)
    

    このうちmethod_nameは一意のキーであり、このようにしても、ロック解除時に改行記録を直接削除すればよい.しかし、このようにして、ロックはブロック式ではありません.挿入データはすぐに結果を返すことができるからです.
    では、上記のデータベースで実現される2つの分散ロックについて、どのようなメリットとデメリットがありますか?
    メリット
    簡単、便利、迅速な実現
    欠点
  • データベースベースで、オーバーヘッドが大きいため、パフォーマンスに影響を与える可能性があります.
  • データベースの現在の読み取りに基づいて実装され、データベースは最下位レベルで最適化され、インデックスが使用される可能性があり、インデックスが使用されない可能性があります.これはクエリー計画の分析
  • に依存します.
    Zookeeper実装
    ロックの取得
  • まずロックノードがあり、lockRootNode、これは永続的なノード
  • であることができる.
  • クライアントはロックを取得し、lockRootNodeの下に順次の瞬時ノードを作成し、クライアントが接続を切断することを保証し、ノードも自動的に
  • を削除する.
  • lockRootNode親ノードのgetChildren()メソッドを呼び出し、すべてのノードを取得し、小さいから大きいまでソートします.作成された最小ノードが現在のノードであればtrueに戻り、ロックに成功します.そうしないと、自分のシーケンス番号より小さいノードの解放動作(exist watch)に注目します.これにより、各クライアントが1つのノードに注目する必要があることを保証できます.羊の群れ効果を避けるために、すべてのノードに注目する必要はありません.
  • ノード解放操作がある場合は、ステップ3
  • を繰り返す.
    リリースロック
    ステップ2で作成したノードを削除するだけです
    Zookeeperを用いた分散ロックにはどのようなメリットとデメリットがあるのでしょうか.
    メリット
  • クライアントダウンタイム障害が発生した場合、ロックはすぐに
  • を解放することができる.
  • はブロックロックを実現することができ、watcherリスニングによって実現することも比較的簡単である
  • クラスタモード、安定性比較的高い
  • 欠点
  • ネットワークにジッタがあると、Zookeeperはクライアントがダウンタイムしていると判断し、接続を切断し、他のクライアントはロックを取得することができます.もちろんZookeeperには再試行メカニズムがありますが、これはその再試行メカニズムに依存する戦略です.
  • パフォーマンスはキャッシュ
  • に劣る.
    Redis実装
    まず例を挙げます.例えば、今製品の情報を更新します.製品の唯一のキーはproductIdです.
    シンプルな実装1
    public boolean lock(String key, V v, int expireTime){
            int retry = 0;
            //         10 
            while (retry < failRetryTimes){
                //   
                Boolean result = redis.setNx(key, v, expireTime);
                if (result){
                    return true;
                }
    
                try {
                    //             
                    TimeUnit.MILLISECONDS.sleep(sleepInterval);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
    
            }
    
            return false;
        }
        public boolean unlock(String key){
            return redis.delete(key);
        }
        public static void main(String[] args) {
            Integer productId = 324324;
            RedisLock redisLock = new RedisLock();
            redisLock.lock(productId+"", productId, 1000);
        }
    }
    

    これは簡単な実装であり、問題があります.
  • は、現在のスレッドのロックが他のスレッドによって誤って解放される可能性がある.例えば、aスレッドがロックを取得して実行中であるが、内部フロー処理がタイムアウトしたりgcがロックが期限切れになったりしたため、このときbスレッドがロックを取得し、aとbスレッドが同じproductIdを処理し、bはまだ処理中であり、このときa処理が完了し、aがロックを解放し、aがbで取得したロックを解放する可能性があります.
  • は再入可能な
  • を実現できない.
  • クライアントは、最初に設定が成功したが、タイムアウトの戻りに失敗した場合、その後、クライアントの試行は
  • に失敗する.
    以上の問題について改善します.
  • vはrequestIdを伝え、ロックを解除するときに、現在のrequestIdであれば解放できると判断します.そうしないと
  • は解放できません.
  • countのロックカウントを加え、ロックを取得するときに一度クエリーし、現在のスレッドがすでに持っているロックであれば、ロック技術に1を加え、true
  • に直接戻る.
    シンプルな実装2
    private static volatile int count = 0;
    public boolean lock(String key, V v, int expireTime){
        int retry = 0;
        //         10 
        while (retry < failRetryTimes){
            //1.    ,           ,     
            //2.         ,       ,              ,             
            V value = redis.get(key);
            //       ,          ,    +1,    
            if (null != value && value.equals(v)){
                count ++;
                return true;
            }
    
            //         ,         
            if (value == null || count <= 0){
                //   
                Boolean result = redis.setNx(key, v, expireTime);
                if (result){
                    count = 1;
                    return true;
                }
            }
    
            try {
                //             
                TimeUnit.MILLISECONDS.sleep(sleepInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
    
        }
    
        return false;
    }
    public boolean unlock(String key, String requestId){
        String value = redis.get(key);
        if (Strings.isNullOrEmpty(value)){
            count = 0;
            return true;
        }
        //                ,        ,      false
        if (value.equals(requestId)){
            if (count > 1){
                count -- ;
                return true;
            }
            
            boolean delete = redis.delete(key);
            if (delete){
                count = 0;
            }
            return delete;
        }
    
        return false;
    }
    public static void main(String[] args) {
        Integer productId = 324324;
        RedisLock redisLock = new RedisLock();
        String requestId = UUID.randomUUID().toString();
        redisLock.lock(productId+"", requestId, 1000);
    }
    

    この実装は、誤放出および再入可能な問題を基本的に解決し、ここではいくつかの点を説明する.
  • countを導入して再入を実現すると、業務のニーズを見て、しかもロックを解放する時、実は直接ロックを削除することができて、1回の解放は終わって、countの数量を通じて何度も解放する必要はありませんて、業務のニーズを見ましょう
  • ロックの設定タイムアウトを考慮するため、ロックを設定する際に一度問い合わせる必要があり、性能の考慮があるかもしれませんが、具体的な業務を見てみましょう
  • .
  • 現在ロック取得に失敗した待機時間はコードに設定されており、待機論理を修正すれば
  • になる.
    エラー実装
    ロックを取得した後、ロックの有効期限を確認します.ロックが有効期限切れになった場合、次のようにします.
    public boolean tryLock2(String key, int expireTime){
        long expires = System.currentTimeMillis() + expireTime;
    
        //    
        Boolean result = redis.setNx(key, expires, expireTime);
        if (result){
            return true;
        }
    
        V value = redis.get(key);
        if (value != null && (Long)value < System.currentTimeMillis()){
            //      
            String oldValue = redis.getSet(key, expireTime);
            if (oldValue != null && oldValue.equals(value)){
                return true;
            }
        }
        
        return false;
    }
    

    このような実装の問題は,現在のサーバの時間に過度に依存し,大量の同時要求の下でロックの期限切れが判断され,このときにロックを設定すると,最終的には1つのスレッドしか存在しないが,最終的にロックを取得したスレッド設定の時間を異なるサーバが自己の時間によって上書きする可能性がある.
    Tair実装
    Tairによる分散ロックとRedisの実装コアの差は多くありませんが、Tairには便利なapiがあり、分散ロックを実現するための最適な構成と感じられます.Put api呼び出し時にversionを入力する必要があります.データベースの楽観的なロックと同じように、データを変更すると、バージョンが自動的に加算され、入力されたバージョンと現在のデータバージョンが一致しない場合、変更は許可されません.