Redis分散ロックの正しい実装方法(ベース)


Redis分布式ロックの正確な実現方式前言分布式ロックには一般的に3つの実現方式がある.
  • データベース楽観ロック;
  • Redisベースの分散ロック;
  • は、ZooKeeperの分散ロックに基づいている.このブログでは、Redisに基づいて分散ロックを実現する第2の方法について説明します.このブログでは、Redis分散ロックの実装について様々なブログが紹介されていますが、彼らの実装には様々な問題があり、子弟の誤解を避けるために、Redis分散ロックを正しく実装する方法について詳しく説明します.

  • 信頼性はまず、分散ロックが利用可能であることを保証するために、少なくともロックの実装が次の4つの条件を満たすことを保証しなければならない:反発性.任意の時点で、ロックを保持できるクライアントは1つしかありません.デッドロックは発生しません.ロックを保持している間にクラッシュし、アクティブにロックを解除しなくても、後続の他のクライアントがロックを追加できることを保証します.ほとんどのRedisノードが正常に動作している限り、クライアントはロックとロック解除を行うことができます.ベルを解くには、ベルを結ぶ人のロックとロックを解く必要があります.同じクライアントでなければなりません.クライアントは自分で他の人のロックを解くことはできません.
    コンポーネント依存性まず我々はMavenを通じてJedisオープンソースコンポーネントを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行表示されますset(String key,String value,String nxxx,String expx,int time)、このset()メソッドには5つのパラメータがあります.
    1つ目は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よくあるエラー例はjedissを用いることである.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
    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;
    
    }
    

    このエラー例では、問題の発見が困難であり、実装も複雑である.実現構想:jedisを用いる.setnx()コマンドは、keyがロックであり、valueがロックの期限切れであるロックを実現します.実行プロセス:1.setnx()メソッドでロックを試み、現在のロックが存在しない場合はロックに戻ります.2.ロックがすでに存在する場合は、ロックの有効期限を取得し、現在の時間と比較して、ロックが有効期限切れの場合は、新しい有効期限を設定し、ロックが正常に行われたことを返します.コードは次のとおりです.
    では、このコードの問題はどこですか.1.クライアント自身が期限切れ時間を生成するため、分散型で各クライアントの時間を同期させる必要がある.2.ロックが期限切れになると、複数のクライアントがjedissを同時に実行する.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コマンドの一部を説明します.
    簡単に言えば、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がマルチマシンで導入されている場合は、Redissonを使用して分散ロックを実装してみてください.これはRedisが公式に提供しているJavaコンポーネントです.リンクは参照してください.