redisによる分散錠(一)


分散式錠の解決方法
  • は、データベーステーブルに基づいて楽観的なロックを行い、分散式のロックに用いられる.(小合併に適用)
  • は、分散ロックのためのmemcachedのadd()方法を使用する.
  • は、分散ロックのためのmemcachedのcas()方法を使用する.(非常用)
  • は、分布式錠のためにredisのsetnx()、expire()の方法を使用する.
  • は、分布式錠のためにredisのsetnx()、get()、getset()の方法を使用する.
  • は、redisのウォッチ、multi、execコマンドを使用して、分散式のロックに使用します.(非常用)
  • は、zookeeperを使用して、分散式ロックに使用されます.(非常用)
  • ここでは主に第四種類と第五種類を紹介します.
    redisのsetnx()、expire()の方法を使用して、分散式錠に使用します.
    原理
    redisを用いたsetnx()、expire()を用いて分散ロックを実現するために、このスキームはmemcached()のadd()スキームに対して、redisが優勢となるのは、サポートされているデータタイプがより多く、memcachedはSteringのデータタイプのみをサポートすることである.それ以外に、性能の上から言っても、操作の便利性から言っても、実はすべて多すぎる違いがなくて、完全にあなたの選択を見て、たとえば会社の中でどれを使うのが比較的に多いですか?あなたはどれを使うことができますか?
    まずsetnx()コマンドを説明します.setnxとはSET if Not Existsの意味です.主に二つのパラメータsetnx(key,value)があります.この方法は原子であり,keyが存在しない場合は,現在のkeyを設定して成功し,1を返す.現在のkeyが既に存在している場合、現在のkeyを設定できませんでした.0を返します.ただし、setnxコマンドではkeyのタイムアウト時間を設定できません.expire()でkeyを設定するしかありません.
    具体的な使用手順は以下の通りです.
  • setnx(lockkey,1)が0に戻ると、占有率の失敗を説明する.1を返したら、ビット成功
  • を示します.
  • expire()コマンドはロックの問題を避けるためにタイムアウト時間を設定します.
  • 業務コードを実行した後、deleteコマンドでkeyを削除することができます.
  • あるRedisノードが利用できない時にアルゴリズムが動作し続けることを保証するために、この取得ロックの動作は、ロックの有効時間(数十ミリ秒級)よりもはるかに小さいタイムアウト時間が必要である.
    あり得る問題
    この方案は実は日常の仕事の中の需要を解決することができますが、技術案の検討から言えば、まだ改善できるところがあります.例えば、第1ステップのsetnx実行が成功した後に、expireコマンドが実行される前に、あたごの現象が発生すると、依然としてデッドロックの問題が発生しますので、完全にするには、redisのsetnx()、get()とgetsset()の方法を使って分散式のロックを実現することができます.
    具体的に実現する
    ロックは具体的にRedis Lockを実現します.
    
    package com.xiaolyuh.lock;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    public class RedisLock {
        private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
    
        ////////////////////         ///////////////////////
        /**
         *    redis     
         */
        private static final String LOCKED = "LOCKED";
    
        /**
         *           (ms   )
         */
        private static final long TIME_OUT = 100;
    
        /**
         *         (s)
         */
        public static final int EXPIRE = 60;
        ////////////////////         ///////////////////////
    
        /**
         *       key
         */
        private String key;
    
        /**
         *       (s)
         */
        private int expireTime = EXPIRE;
    
        /**
         *         (ms)
         */
        private long timeOut = TIME_OUT;
    
        /**
         *  flag
         */
        private volatile boolean isLocked = false;
        /**
         * Redis    
         */
        private StringRedisTemplate redisTemplate;
    
        /**
         *     
         *
         * @param redisTemplate Redis    
         * @param key             key
         * @param expireTime          ( )
         * @param timeOut               (  )
         */
        public RedisLock(StringRedisTemplate redisTemplate, String key, int expireTime, long timeOut) {
            this.key = key;
            this.expireTime = expireTime;
            this.timeOut = timeOut;
            this.redisTemplate = redisTemplate;
        }
    
        /**
         *     
         *
         * @param redisTemplate Redis    
         * @param key             key
         * @param expireTime         
         */
        public RedisLock(StringRedisTemplate redisTemplate, String key, int expireTime) {
            this.key = key;
            this.expireTime = expireTime;
            this.redisTemplate = redisTemplate;
        }
    
        /**
         *     (         30 ,     60 )
         *
         * @param redisTemplate Redis    
         * @param key             key
         */
        public RedisLock(StringRedisTemplate redisTemplate, String key) {
            this.key = key;
            this.redisTemplate = redisTemplate;
        }
    
        public boolean lock() {
            //       ,  
            long nowTime = System.nanoTime();
            //        ,  
            long timeout = timeOut * 1000000;
            final Random random = new Random();
    
            //      Master     ,     (System.nanoTime() - nano)               
            //                  master         
            //     master      ,         master  
            while ((System.nanoTime() - nowTime) < timeout) {
                //     key   redis   ,        
                if (redisTemplate.opsForValue().setIfAbsent(key, LOCKED)) {
                    isLocked = true;
                    //        ,          ,                            
                    //                         
                    redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    
                    //         
                    break;
                }
                //       ,            ,                         
                //   10        
                try {
                    Thread.sleep(10, random.nextInt(50000));
                } catch (InterruptedException e) {
                    logger.error("           :", e);
                }
            }
            return isLocked;
    
        }
    
        public boolean isLock() {
            redisTemplate.getConnectionFactory().getConnection().time();
            return redisTemplate.hasKey(key);
        }
    
        public void unlock() {
            //    
            //          ,      ,             
            if (isLocked) {
                redisTemplate.delete(key);
            }
        }
    
    }
    
    
    ロックを呼び出す:
    
    public void redisLock(int i) {
            RedisLock redisLock = new RedisLock(redisTemplate, "redisLockKey:"+i % 10, 5*60 , 500);
            try {
                long now = System.currentTimeMillis();
                if (redisLock.lock()) {
                    logger.info("=" + (System.currentTimeMillis() - now));
                    // TODO            
                    logger.info("j:" + j ++);
                } else {
                    logger.info("k:" + k ++);
                }
            } catch (Exception e) {
                logger.info(e.getMessage(), e);
            } finally {
                //       
                redisLock.unlock();
            }
        }
    
    redisのsetnx()、get()、getsset()の方法を使って、分布式の鎖に用います.
    原理
    この方案の背景は主にsetnx()とexpire()の方案の上で存在するかもしれないデッドロックの問題に対して一版の最適化をしました.
    まずこの三つの命令を説明します.setnx()とget()の二つの命令については、これ以上何も言わないでください.ではgetsset()コマンド?このコマンドは主に二つのパラメータgetsetがあります.この方法は原子であり、keyにはnewValueという値を設定し、keyの元の値に戻す.そもそもkeyが存在しないと仮定すると、このコマンドを何度も実行すると、次のような効果が現れます.
  • getset(key,value 1)はnilに戻ります.このときkeyの値はvalue 1
  • に設定されます.
  • getset(key,value 2)がvalue 1に戻ると、keyの値がvalue 2
  • に設定されます.
  • 順に類推します.
  • 使用するコマンドを紹介したら、具体的な使用手順は以下の通りです.
  • setnx(lockkey、現在時間+期限切れタイムアウト時間)は、1を返したらロックに成功します.0に戻るとロックが取れず、2に移る.
  • get(lockkey)は、値oldExpireTimeを取得し、このvalue値を現在のシステム時間と比較し、現在のシステム時間より小さい場合、このロックはすでにタイムアウトしたと見なし、他の要求を再取得し、3に移ることができる.
  • は、newExpireTime=現在の時間+期限切れのタイムアウト時間を計算し、そしてgetset(lockkey、newExpireTime)は、現在のlockkeyの値current ExpireTimeに戻ります.
  • は、current ExpireTimeとoldExpireTimeが等しいかどうかを判断し、等しい場合、現在のgetsset設定が成功したと説明し、ロックを取得した.もし同じではないならば、このロックはまた他の要求によって取得されたと説明しています.現在の要求は直接に失敗に戻ります.または再試行を続けます.
  • ロックを取得した後、現在のスレッドは自分の業務処理を開始することができ、処理が完了した後、自分の処理時間とロックに対するタイムアウト時間を比較し、ロック設定のタイムアウト時間より小さい場合は、Deleteリリースロックを直接実行する.ロック設定のタイムアウト時間より大きい場合、再度ロックして処理する必要はありません.
  • あり得る問題
    問題1: 「get(lockkey)取得値oldExpireTime」という操作と「getset(lockkey,newExpireTime)」という操作の間に、N個のスレッドがあるとget操作で同じoldExpireTimeを取得した後、getsetに行って、戻ってくるnewExpireTimeは同じですか?
    この案にはこの問題がないと思います.二つの方法によると、第一に、Redisはシングルプロセスのシングルスレッドモードであり、シリアルでコマンドを実行する.第二に、シリアル実行の前提条件で、getssetの後に戻ってくるcurrent ExpireTimeとoldExpireTimeが等しいかどうかを比較する.
    問題2: 「get(lockkey)取得値oldExpireTime」という操作と「getset(lockkey,newExpireTime)」という動作の間に、N個のスレッドがget操作で同じoldExpireTimeを取得した後、すべてgetsetに行き、第1スレッドのロック取得に成功したと仮定して、彼はロック取得に失敗しましたが、スレッドを取得するために失敗しました.このようにして、最初のロックを取得するスレッドのタイムアウト時間が長くなりますか?
    この案は確かにこの問題の可能性があると思います.しかし、個人的にはこの笑いの誤差は無視できると思いますが、技術案には欠陥があります.皆さんは自分で選んでください.
    問題3: このプログラムは分散サーバの時間を必ず同期させなければなりません.そうでないと、このロックが問題になります.
    具体的に実現する
    ロックは具体的にRedis Lockを実現します.
    
    package com.xiaolyuh.lock;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.dao.DataAccessException;
    import org.springframework.data.redis.connection.RedisConnection;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Redis    (              ,      )
     * 
     * @author yuhao.wangwang
     * @version 1.0
     * @date 2017 11 3    10:21:27
     */
    public class RedisLock2 {
    
        /**
         *           (ms   )
         */
        private static final long TIME_OUT = 100;
    
        /**
         *         (s)
         */
        public static final int EXPIRE = 60;
    
        private static Logger logger = LoggerFactory.getLogger(RedisLock2.class);
    
        private StringRedisTemplate redisTemplate;
    
        /**
         *       key
         */
        private String lockKey;
        /**
         *       (s)
         */
        private int expireTime = EXPIRE;
    
        /**
         *         (ms)
         */
        private long timeOut = TIME_OUT;
    
        /**
         *       
         */
        private long expires = 0;
    
        /**
         *    
         */
        private volatile boolean locked = false;
    
        final Random random = new Random();
    
        /**
         *                    
         *
         * @param redisTemplate
         * @param lockKey         key(Redis Key)
         */
        public RedisLock2(StringRedisTemplate redisTemplate, String lockKey) {
            this.redisTemplate = redisTemplate;
            this.lockKey = lockKey + "_lock";
        }
    
        /**
         *              ,        
         *
         * @param redisTemplate
         * @param lockKey         key(Redis Key)
         * @param expireTime          (  : )
         */
        public RedisLock2(StringRedisTemplate redisTemplate, String lockKey, int expireTime) {
            this(redisTemplate, lockKey);
            this.expireTime = expireTime;
        }
    
        /**
         *            ,          
         *
         * @param redisTemplate
         * @param lockKey         key(Redis Key)
         * @param timeOut               (  :  )
         */
        public RedisLock2(StringRedisTemplate redisTemplate, String lockKey, long timeOut) {
            this(redisTemplate, lockKey);
            this.timeOut = timeOut;
        }
    
        /**
         *                       
         *
         * @param redisTemplate
         * @param lockKey         key(Redis Key)
         * @param expireTime          (  : )
         * @param timeOut               (  :  )
         */
        public RedisLock2(StringRedisTemplate redisTemplate, String lockKey, int expireTime, long timeOut) {
            this(redisTemplate, lockKey, expireTime);
            this.timeOut = timeOut;
        }
    
        /**
         * @return     key
         */
        public String getLockKey() {
            return lockKey;
        }
    
        /**
         *    lock.
         *     :       redis  setnx  ,    .
         * reids   key   key,     , value       (  :         value ,            )
         *     :
         * 1.  setnx      key  ,  (       )   ,     
         * 2.              ,       ,    ,      
         *
         * @return true if lock is acquired, false acquire timeouted
         * @throws InterruptedException in case of thread interruption
         */
        public boolean lock() {
            //        ,  
            long timeout = timeOut * 1000000;
            //       ,  
            long nowTime = System.nanoTime();
    
            while ((System.nanoTime() - nowTime) < timeout) {
                //          ,   1     
                expires = System.currentTimeMillis() + expireTime + 1;
                String expiresStr = String.valueOf(expires); //     
    
                if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) {
                    locked = true;
                    //        ,          ,                            
                    //                         
                    redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
    
                    //         
                    return true;
                }
    
                String currentValueStr = redisTemplate.opsForValue().get(lockKey); //redis    
                if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                    //      ,       ,           ,             
                    // lock is expired
    
                    String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr);
                    //          ,           ,
                    //                    ,  jedis.getSet    
                    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                        //    (  ,  key    )     ——       ,       ,              ,      
    
                        //[       ]:      ,           ,                  ,        
                        // lock acquired
                        locked = true;
                        return true;
                    }
                }
    
                /*
                      10   ,                ,           , ,         ,
                              ,              ,         ,          ,                .
                                         
                 */
                try {
                    Thread.sleep(10, random.nextInt(50000));
                } catch (InterruptedException e) {
                    logger.error("           :", e);
                }
    
            }
            return locked;
        }
    
    
        /**
         *   
         */
        public synchronized void unlock() {
            //                  
            if (locked && expires > System.currentTimeMillis()) {
                redisTemplate.delete(lockKey);
                locked = false;
            }
        }
    
    }
    
    呼び出し方法:
    
    public void redisLock2(int i) {
        RedisLock2 redisLock2 = new RedisLock2(redisTemplate, "redisLock:" + i % 10, 5 * 60, 500);
        try {
            long now = System.currentTimeMillis();
            if (redisLock2.lock()) {
                logger.info("=" + (System.currentTimeMillis() - now));
                // TODO            
                logger.info("j:" + j++);
            } else {
                logger.info("k:" + k++);
            }
        } catch (Exception e) {
            logger.info(e.getMessage(), e);
        } finally {
            redisLock2.unlock();
        }
    }
    
    上記の二つのredisに対して分散式のロックを実現する方案には一つの問題があります.
  • は、ロックを取得してから業務ロジックを実行するコードは、redisロックの有効時間内しかないです.redis keyが期限切れになったら自動的にクリアされます.このロックは解除されます.このロックの有効時間は必ず業務に合わせて評価します.
  • この2つの方式はロック解除時に直接キーを削除し、C 1がロックを取得した場合には、redisが保留され、データが持続化されず、redisサービスが起動したら、C 2はロックを取得するように要求します.しかし、C 1の要求は現在実行済みで、キーを削除しました.この時、C 2のロックを削除しました.(次の文章に解決案がある)
  • redisのSET reource-name anystring NX EX max-lock-time方式を使って分散式ロックを実現します.
    次の記事ではSpring-data-redis+redis分布錠(二)を紹介します.
    ソース: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
    spring-book-student-data-redis-distributed-lock工程
    参考:
  • http://www.cnblogs.com/PurpleDream/p/5559352.html
  • https://www.cnblogs.com/0201zcr/p/5942748.html
  • http://zhangtielei.com/posts/blog-redlock-reasoning.html
  • http://strawhatfy.github.io/2015/07/09/Distributed%20locks%20with%20Redis/
  • http://blog.csdn.net/supper10090/article/details/77851512