redis分散ロックjava版

7935 ワード

ターンhttps://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/
前言
分散ロックには一般的に3つの実現方式がある:1.データベース楽観ロック;2.Redisベースの分散ロック;3.ZooKeeperベースの分散ロック.このブログでは、Redisに基づいて分散ロックを実現する第2の方法について説明します.このブログでは、Redis分散ロックの実装について様々なブログが紹介されていますが、彼らの実装には様々な問題があり、子弟の誤解を避けるために、Redis分散ロックを正しく実装する方法について詳しく説明します.
しんらいせい
まず、分散ロックが使用可能であることを確認するために、少なくともロックの実装が以下の4つの条件を満たしていることを確認します.
  • 相互反発性.任意の時点で、ロックを保持できるクライアントは1つしかありません.
  • デッドロックは発生しません.ロックを保持している間にクラッシュし、アクティブにロックを解除しなくても、後続の他のクライアントがロックを追加できることを保証します.
  • はフォールトトレランスを有する.ほとんどのRedisノードが正常に動作している限り、クライアントはロックとロック解除を行うことができます.
  • ベルを解くにはベルを結ぶ人が必要です.ロックとロック解除は同じクライアントでなければなりません.クライアントは自分で他の人のロックを解除することはできません.

  • コード実装
    コンポーネント依存
    まず、JedisオープンソースコンポーネントをMavenで導入し、pom.xmlファイルに次のコードを追加します.
    
        redis.clients
        jedis
        2.9.0
    
    
    

    ロックコード
    正しい姿勢
    Talk is cheap, show me the code.まずコードを示し、なぜこのように実現されたのかをゆっくり説明します.
    public class RedisTool {
    
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
    
        /**
         *         
         * @param jedis Redis   
         * @param lockKey  
         * @param requestId     
         * @param expireTime     
         * @return       
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
    
        }
    
    }
    
    

    1行のコード:jedis.set(String key, String value, String nxxx, String expx, int time)、このset()メソッドには5つのパラメータがあります.
  • は最初にkeyであり、keyが唯一であるため、keyをロックとして使用します.
  • の2番目はvalueで、私たちが伝えたのはrequestIdで、多くの子供靴は分からないかもしれませんが、keyが鍵としてあれば十分ではありませんか.どうしてvalueを使うのですか.なぜなら,信頼性について上述した場合,分散ロックが4番目の条件を満たすにはベルを外す必要があり,valueにrequestIdを付与することで,このロックがどのリクエストに加えられたのか,ロックを解除する際に根拠があるからである.requestIdは、UUID.randomUUID().toString()メソッドを使用して生成することができる.
  • の3番目はnxxxで、このパラメータはNXで記入して、SET IF NOT EXISTを意味して、つまりkeyが存在しない時、私たちはset操作を行います;keyがすでに存在する場合、何もしません.
  • の4番目はexpxで、このパラメータはPXを伝えています.これは私たちがこのkeyに期限切れの設定を追加することを意味しています.具体的な時間は5番目のパラメータによって決まります.
  • の5番目はtimeであり、4番目のパラメータに呼応し、keyの期限切れ時間を表す.

  • 総じて、上記のset()メソッドを実行すると、2つの結果しか得られません:1.現在ロックが存在しない(keyが存在しない)場合は、ロック操作を行い、ロックに有効期間を設定し、valueはロックされたクライアントを表します.2.ロックは既に存在し、何もしない.
    心の細い子供靴は、私たちのロックコードが私たちの信頼性に記述されている3つの条件を満たしていることを発見します.まず、set()にはNXパラメータが追加され、keyが存在する場合、関数が正常に呼び出されないことを保証することができます.つまり、ロックを保持し、反発性を満たすクライアントは1つしかありません.次に、ロックに期限切れが設定されているため、ロックの所有者がその後クラッシュしてロックを解除しなくても、ロックは期限切れになると自動的にロックを解除(すなわちkeyが削除される)し、デッドロックは発生しません.最後に、valueはrequestIdとして割り当てられ、ロックされたクライアント要求IDを表すため、クライアントがロックを解除したときに同じクライアントであるかどうかを検証することができます.Redisスタンドアロン配置のシナリオのみを考慮するため,フォールトトレランスはしばらく考慮しない.
    エラー例1
    一般的なエラーの例は、jedis.setnx()jedis.expire()の組み合わせを使用してロックを実装することです.コードは以下のとおりです.
    public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
        Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            //           ,         ,     
            jedis.expire(lockKey, expireTime);
        }
    
    }
    
    

    setnx()メソッドの役割はSET IF NOT EXISTであり,expire()メソッドはロックに期限切れ時間を加えることである.一見、前述のset()メソッドの結果と同じように見えるが、これは2つのRedisコマンドであり、原子性がないため、プログラムがsetnx()を実行した後に突然クラッシュした場合、ロックに期限切れ時間が設定されない.デッドロックが発生しますネット上でこのように実現されたのは,低バージョンのjedisがマルチパラメータのset()メソッドをサポートしていないためである.
    エラー例2
    このエラー例では、問題の発見が困難であり、実装も複雑である.実装構想:jedis.setnx()コマンドを使用してロックを実装し、keyはロックであり、valueはロックの期限切れである.実行プロセス:1.setnx()メソッドでロックを試み、現在のロックが存在しない場合はロックに戻ります.2.ロックがすでに存在する場合は、ロックの有効期限を取得し、現在の時間と比較して、ロックが有効期限切れの場合は、新しい有効期限を設定し、ロックが正常に行われたことを返します.コードは次のとおりです.
    public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
    
        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);
    
        //         ,      
        if (jedis.setnx(lockKey, expiresStr) == 1) {
            return true;
        }
    
        //      ,        
        String currentValueStr = jedis.get(lockKey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            //     ,           ,           
            String oldValueStr = jedis.getSet(lockKey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                //           ,                ,       
                return true;
            }
        }
    
        //     ,        
        return false;
    
    }
    
    

    では、このコードの問題はどこですか.1.クライアント自身が期限切れ時間を生成するため、分散型で各クライアントの時間を同期させる必要がある.2.ロックが期限切れになった場合、複数のクライアントが同時にjedis.getSet()の方法を実行する場合、最終的には1つのクライアントのみがロックを追加することができるが、このクライアントのロックの期限切れ時間は他のクライアントによって上書きされる可能性がある.3.ロックには所有者IDがありません.つまり、どのクライアントでもロックを解除できます.
    ロック解除コード
    正しい姿勢
    まずコードを見せて、なぜこのように実現したのかをゆっくり説明します.
    public class RedisTool {
    
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
         *       
         * @param jedis Redis   
         * @param lockKey  
         * @param requestId     
         * @return       
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
    
        }
    
    }
    
    

    ロックを解除するには2行のコードしか必要ありません.最初の行のコード、私たちは簡単なLuaスクリプトコードを書いて、前回このプログラミング言語を見たのはやはり「ハッカーと画家」の中で、今回意外にも使ったとは思わなかった.2行目のコードでは、Luaコードをjedis.eval()メソッドに転送し、パラメータKEYS[1]をlockKeyに、ARGV[1]をrequestIdに割り当てます.eval()メソッドはLuaコードをRedisサービス側に渡して実行する.
    では、このLuaコードの機能は何でしょうか.実は簡単です.まずロックに対応するvalue値を取得し、requestIdと等しいかどうかを確認し、等しい場合はロックを削除します(ロック解除).では、なぜLua言語を使って実現するのでしょうか.上記の操作が原子性であることを確保するためである.非原子性がどのような問題をもたらすかについては、【ロック解除コード-エラー例2】を参照してください.では、なぜeval()メソッドを実行すると原子性が確保されるのかは、Redisの特性に由来し、以下は公式サイトのevalコマンドの一部を説明します.
    [画像のアップロードに失敗しました...(image-6 d 809 d-1556589888199)]
    簡単に言えば、evalコマンドがLuaコードを実行すると、Luaコードはコマンドとして実行され、evalコマンドの実行が完了するまで、Redisは他のコマンドを実行しません.
    エラー例1
    最も一般的なロック解除コードは、jedis.del()メソッドを直接使用してロックを削除することです.このようなロックの所有者を先に判断せずに直接ロックを解除する方法では、このロックがそれではない場合でも、どのクライアントもいつでもロックを解除することができます.
    public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }
    
    

    エラー例2
    このロック解除コードは一見問題なく、私ももう少しで実現するところでしたが、正しい姿勢とは差が少なく、唯一の違いは2つのコマンドに分けて実行することです.コードは以下の通りです.
    public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    
        //                 
        if (requestId.equals(jedis.get(lockKey))) {
            //     ,             ,     
            jedis.del(lockKey);
        }
    
    }
    
    

    コード注釈のように、問題は、jedis.del()メソッドが呼び出された場合、このロックが現在のクライアントに属していない場合、他のユーザが追加したロックが解除されることである.では、本当にこのようなシーンがありますか?答えは肯定的で、例えばクライアントAがロックをかけて、しばらくしてからクライアントAがロックを解除して、jedis.del()を実行する前に、ロックが突然期限切れになって、この時クライアントBがロックを試みて成功して、それからクライアントAがdel()方法を実行して、クライアントBのロックを解除しました.
    まとめ
    この論文では主にJavaコードを用いてRedis分散ロックを正しく実装する方法を紹介し,ロックとロック解除についてもそれぞれ2つの比較的古典的なエラー例を示した.実はRedisによって分散ロックを実現するのは難しくなく,信頼性の4つの条件を満たすことを保証すればよい.インターネットは私たちに便利さをもたらしてくれたが、問題があればgoogleできるが、ネット上の答えはきっと正しいのだろうか.実はそうではないので、私たちは常に疑問の精神を維持し、多くの検証を考えなければならない.
    プロジェクト内でRedisがマルチマシンで導入されている場合は、Redisが公式に提供するJavaコンポーネントであるRedissonを使用して分散ロックを実装してみてください.
    リファレンス
    [1] Distributed locks with Redis
    [2] EVAL command
    [3] Redisson