Redisは分散ロックとして機能する(一):SpringBoot集積Redisson分散ロック

40983 ワード

文書ディレクトリ
  • 一、プロジェクトDemo
  • 1.1プロジェクト依存
  • 1.2プロジェクト構成-redis
  • 1.3 Redissonの構成クラスRedissonConfig
  • 1.4は、分散ロックのいくつかの動作
  • のためのLokerインターフェースを定義する.
  • 1.5 Redissonベースの実装クラスRedissonLocker
  • 1.6分散ロックツールクラスLockUtil
  • を定義
  • 1.7ユニットテスト
  • 1.8ソースコード
  • 二、RedissonのRedlock
  • 2.1 Redisコマンドに基づく分散ロック
  • 2.2 Redlock
  • を実現
  • 2.3 Redlockソース
  • 2.3.1一意ID
  • 2.3.2取得ロック
  • 2.3.2リリースロック
  • Redissonは分散型と拡張性のJavaデータ構造を実現し、Jedisに比べて機能が簡単で、文字列操作をサポートせず、ソート、トランザクション、パイプ、パーティションなどのRedis特性をサポートしない.Redissonの目的は,利用者のRedisへの関心の分離を促進し,利用者がビジネスロジックの処理に集中できるようにすることである.
    一、プロジェクトDemo
    1.1プロジェクト依存
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
    
        org.redisson
        redisson
        3.10.6
    
    

    1.2プロジェクト構成-redis
    spring:
      redis:
        host: 47.98.178.84
        port: 6379
        database: 0
        password: password
        timeout: 60s
    

    1.3 Redissonの構成クラスRedissonConfig
    @Configuration
    public class RedissonConfig {
    
        @Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private String port;
        @Value("${spring.redis.password}")
        private String password;
    
        /**
         * RedissonClient,    
         * @return
         * @throws IOException
         */
        @Bean(destroyMethod = "shutdown")
        public RedissonClient redisson() throws IOException {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
            return Redisson.create(config);
        }
    
        @Bean
        public RedissonLocker redissonLocker(RedissonClient redissonClient){
            RedissonLocker locker = new RedissonLocker(redissonClient);
            //  LockUtil      
            LockUtil.setLocker(locker);
            return locker;
        }
    }
    

    1.4 Lockerインタフェースを定義し、分散ロックのいくつかの操作に使用する
    主に拡張を考慮するために,本論文ではredissonで実装し,同じLokerインタフェースでもredisで分散ロックを実装することができ,詳細は別の記事を参照する.
    public interface Locker {
        /**
         *    ,      ,           ,       。
         *
         * @param lockKey
         */
        void lock(String lockKey);
    
        /**
         *    
         *
         * @param lockKey
         */
        void unlock(String lockKey);
    
        /**
         *      :            
         *
         * @param lockKey
         * @param timeout :  
         */
        void lock(String lockKey, int timeout);
    
        /**
         *      :            
         *
         * @param lockKey
         * @param unit
         * @param timeout
         */
        void lock(String lockKey, TimeUnit unit, int timeout);
    
        /**
         *      ,       true,        false
         *
         * @param lockKey
         * @return
         */
        boolean tryLock(String lockKey);
    
        /**
         *      ,             true,    false,      ,            ,
         *           leaseTime    
         * @param lockKey
         * @param waitTime
         * @param leaseTime
         * @param unit
         * @return
         */
        boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
                throws InterruptedException;
    
        /**
         *              
         *
         * @param lockKey
         * @return
         */
        boolean isLocked(String lockKey);
    }
    

    1.5 Redissonベースの実装クラスRedissonLocker
    public class RedissonLocker implements Locker{
    
        private RedissonClient redissonClient;
    
        public RedissonLocker(RedissonClient redissonClient) {
            super();
            this.redissonClient = redissonClient;
        }
    
        
        public void lock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
        }
    
    
        public void unlock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.unlock();
        }
    
        
        public void lock(String lockKey, int leaseTime) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(leaseTime, TimeUnit.SECONDS);
        }
    
        
        public void lock(String lockKey, TimeUnit unit, int timeout) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(timeout, unit);
        }
    
        public void setRedissonClient(RedissonClient redissonClient) {
            this.redissonClient = redissonClient;
        }
    
        
        public boolean tryLock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            return lock.tryLock();
        }
    
        
        public boolean tryLock(String lockKey, long waitTime, long leaseTime,
                               TimeUnit unit) throws InterruptedException{
            RLock lock = redissonClient.getLock(lockKey);
            return lock.tryLock(waitTime, leaseTime, unit);
        }
    
        
        public boolean isLocked(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            return lock.isLocked();
        }
    }
    

    1.6分散ロックツールクラスLockUtilを定義する
    public class LockUtil {
        private static Locker locker;
    
        /**
         *         locker
         * @param locker
         */
        public static void setLocker(Locker locker) {
            LockUtil.locker = locker;
        }
    
        /**
         *    
         * @param lockKey
         */
        public static void lock(String lockKey) {
            locker.lock(lockKey);
        }
    
        /**
         *    
         * @param lockKey
         */
        public static void unlock(String lockKey) {
            locker.unlock(lockKey);
        }
    
        /**
         *    ,    
         * @param lockKey
         * @param timeout
         */
        public static void lock(String lockKey, int timeout) {
            locker.lock(lockKey, timeout);
        }
    
        /**
         *    ,    ,      
         * @param lockKey
         * @param unit
         * @param timeout
         */
        public static void lock(String lockKey, TimeUnit unit, int timeout) {
            locker.lock(lockKey, unit, timeout);
        }
    
        /**
         *      ,       true,        false
         * @param lockKey
         * @return
         */
        public static boolean tryLock(String lockKey) {
            return locker.tryLock(lockKey);
        }
    
        /**
         *      ,    waitTime     ,     true,      false,        leaseTime      
         * @param lockKey
         * @param waitTime
         * @param leaseTime
         * @param unit
         * @return
         * @throws InterruptedException
         */
        public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
                                      TimeUnit unit) throws InterruptedException {
            return locker.tryLock(lockKey, waitTime, leaseTime, unit);
        }
    
        /**
         *             
         * @param lockKey
         * @return
         */
        public static boolean isLocked(String lockKey) {
            return locker.isLocked(lockKey);
        }
    }
    

    1.7ユニットテスト
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class LockerTest {
    
        private static final Logger logger = LoggerFactory.getLogger(LockerTest.class);
    
        @Test
        public void testLock() {
            LockUtil.lock("hello");
            logger.info("    ");
            try {
                //TODO    
            } catch (Exception e) {
                //    
            }finally{
                //   
                LockUtil.unlock("hello");
                logger.info("   ");
            }
        }
    }
    

    1.8ソース
    SpringBoot統合Redisson分散ロック
    二、RedissonのRedlock
    激しい操作の後、その優位性を見てみましょう.
    2.1 Redisコマンドに基づく分散ロックRedis分散ロックのほとんどの人が考えていることを知っています.
    -    (unique_value   UUID )
    SET resource_name unique_value NX PX 30000
    
    -    (lua   ,     value,     )
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

    この実現方法には3つのポイントがあります.
  • setコマンドはset key value px milliseconds nxを使用します.
  • valueは一意性を有しなければならない.
  • ロックを解除するときはvalue値を検証し、誤ってロックを解除することはできません.

  • 同時に、彼にも大きな問題があります.それはロックをかける時に1つのRedisノードにしか作用しません.Redisがsentinelを通じて高可用性を保証しても、このmasterノードが何らかの原因で主従切替が発生した場合、ロックが失われることがあります.
  • はRedisのmasterノードにロックを取得した.
  • しかし、このロックされたkeyはまだslaveノードに同期していない.
  • master障害、障害移行が発生し、slaveノードがmasterノードにアップグレードされます.

  • そのため、Redisの著者antirezは、分散環境に基づいて、より高度な分散ロックの実装方法を提案した:Redlock
    2.2 Redlock実装
    Redisの分散環境では,N個のRedisマスターがあると仮定した.これらのノードは完全に互いに独立しており,主従レプリケーションや他のクラスタ協調機構は存在しない.我々は、Redis単一インスタンスの場合と同様の方法を用いて、N個のインスタンス上でロックを取得および解放することを保証する.ロックを解除するには、クライアントが次の操作を行います.
  • 現在のUnix時間をミリ秒単位で取得します.
  • は、N個のインスタンスから順に、同じkeyおよび一意性を有するvalue(例えばUUID)を使用してロックを取得しようと試みる.Redisにロックの取得を要求する場合、クライアントは、ロックの失効時間よりも小さいネットワーク接続と応答タイムアウト時間を設定する必要があります.たとえば、ロックの自動失効時間が10秒の場合、タイムアウト時間は5~50ミリ秒です.これにより、サーバ側のRedisがすでに停止している場合に、クライアントが応答結果を必死に待っていることを回避することができる.サーバ側が所定時間以内に応答していない場合、クライアントはできるだけ早く別のRedisインスタンスにロックの取得を要求しようとするべきである.
  • クライアントは、現在の時間からロックの取得開始時間(ステップ1で記録された時間)を減算して、ロックの使用を取得する時間を得る.ロックが成功したのは、ほとんどの(N/2+1ノード)のRedisノードからロックが取り外され、使用時間がロックの失効時間よりも小さい場合のみです.
  • ロックが取り外された場合、keyの真の有効時間は、有効時間からロックを取得するために使用される時間を減算することに等しい.
  • 何らかの理由でロックの取得に失敗した場合(少なくともN/2+1個のRedisインスタンスでロックが取得されなかったか、またはロックが有効時間を超えた場合)、クライアントは、すべてのRedisインスタンス上でロック解除を行う必要があります(一部のRedisインスタンスがロックに成功していなくても、一部のノードがロックを取得することを防止しますが、クライアントが応答していないため、次の時間にロックを再取得できません).

  • 2.3 Redlockソースコード
    2.3.1一意ID
    分散ロックを実現する上で非常に重要な点はsetのvalueが一意性を持つことであり、redissonのvalueはどのようにvalueの一意性を保証するのか.答えは**UUID+threadId**です.ソースはRedissonです.JAvaとRedissonLockJAva:
    protected final UUID id = UUID.randomUUID();
    String getLockName(long threadId) {
        return id + ":" + threadId;
    }
    

    2.3.2ロックの取得
    ロックを取得するコードはredLock.tryLock()またはredLock.tryLock(500, 10000, TimeUnit.MILLISECONDS)であり、両方の最終コアソースコードは以下のコードであるが、前者がロックを取得するデフォルトのリース時間(leaseTime)はLOCK_EXPIRATION_INTERVAL_SECONDS、すなわち30 s:
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);
        //        redis      lua  
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  //        KEY    ,       ,    hset  (hset REDLOCK_KEY uuid+threadId 1),   pexpire      (        )
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //        KEY    ,  value   ,           ,       1,        
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //        KEY        
                  "return redis.call('pttl', KEYS[1]);",
                  //          KEYS[1],ARGV[1] ARGV[2]
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
    

    ロックを取得するコマンド:
  • KEYS[1]Collections.singletonList(getName())であり、分布式ロックのkey、すなわちREDLOCK_KEYを表す.
  • ARGV[1]internalLockLeaseTime、すなわちロックされたリース時間であり、デフォルトは30 sである.
  • ARGV[2]getLockName(threadId)であり、ロック時setを取得する唯一の値であるUUID+threadIdである.

  • 2.3.2ロック解除
    ロックを解除するコードはredLock.unlock()で、コアソースは以下の通りです.
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        //        redis      lua  
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                //       KEY   ,   channel      
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                //         ,  value   ,        ,      
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                //               ,        1
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                //      1       0,          ,         ,     
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    //      1      0,          1 ,      KEY,       
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                //  5       KEYS[1],KEYS[2],ARGV[1],ARGV[2] ARGV[3]
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
    
    }