Redis并发锁

作者: softfc | 来源:发表于2019-01-12 15:04 被阅读1次

    使用场景:
    只要有可能由于并发而产生错误数据的地方都需要使用并发锁,而鉴于redis的速度优势和强大的功能,并发锁首先考虑用redis来实现。

    首先需要了解关键的几个redis命令:

    1. SETNX key value
      只有在 key 不存在时才能成功设置 key 的值。
      设置成功,返回 1 。
      设置失败,返回 0 。

    2. GET key
      获取指定的 key 的值。
      当 key 不存在时,返回 nil ,否则,返回 key 的值。
      如果 key 不是字符串类型,那么返回一个错误。

    3. GETSET key value
      将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
      功能同SET key value 和GET key 两个命令一起用。

    死锁:
    在某一段时间段内,有多于一个的进程同时拥有锁,会产生死锁问题。

    加锁流程图:

    Redis并发锁实现流程图.png

    加锁流程解读:

    1. 开始;
    2. 用SETNX命令来加锁,并获取返回值。
    3. 如果返回1,说明加锁成功,返回当前锁,程序结束;
    4. 如果返回0,说明之前有其他进程已经加过锁了,但是不确定加锁的那个进程是否已崩溃,这时候需要GET命令获取到上次的锁,并判断是否已超时。
    5. 如果未超时,则当前进程等待一段时间,再重复第4步;
    6. 如果已超时,则用GETSET重新加锁并返回新锁之前的锁。
    7. 判断GETSET获取到的旧锁是否已经超时(这个判断尤为重要,因为调用GETSET命令之前,或许其他进程也已经执行到了第6步,所以,GETSET命令获取到的有可能是其他进程上得锁);
    8. 如果未超时,则继续执行第5步。
    9. 如果已超时,则把GETSET的新锁返回。程序结束;

    注意:第7步,由于GETSET命令同时执行了set new_value和get old_value两个操作,并对old_value加上超时判断,可以有效避免死锁情况的发生

    下面给出php的实现代码:

    <?php
    /**
     * redis并发锁
     * @author rockyfc
     *
     */
    class RedisLockHelper
    {
        private $_key;
    
        /**
         * @var int 过期时长
         */
        private $_timeout;
    
        /**
         * @var int 锁的过期时间戳
         */
        private $_expires;
    
        /**
         * @var Redis
         */
        private $_redis;
    
    
         /**
         * RedisLockHelper constructor.
         * @param string|array $key 键,请注意,生成键名称最好不要跟其他业务的键名称有重复
         * @param int $timeout 过期时长,默认10秒
         */
        public function __construct($key, $timeout = 10)
        {
            if(is_array($key)){
                $key = serialize($key);
            }
            $this->_key = md5($key);
            $this->_timeout = $timeout;
            $this->_redis = \Yii::$app->redis;
        }
        /**
         * 加锁
         * @param callable|null $callback 业务回调
         * @return bool|mixed
         * @throws \Exception
         */
        public function lock(callable $callback = null)
        {
    
            $lock = 0;
    
            // 获取锁
            while ($lock != 1) {
                $now = time();
                $this->_expires = $now + $this->_timeout + 1;
                $lock = $this->_redis->setnx($this->_key, $this->_expires);
    
                if ($lock == 1 or ($now > $this->_redis->get($this->_key)) and $now > $this->_redis->getset($this->_key, $this->_expires)) {
                    //获取到锁,跳出
                    break;
                } else {
                    //休眠2毫秒
                    usleep(2000);
                }
            }
    
            //执行业务代码
            if ($callback) {
                try{
                    $rs = call_user_func($callback, $this->_expires);
                    $this->unlock();
                    return $rs;
                }catch (\Exception $e){
                    $this->unlock();
                    throw $e;
                }
    
            }
    
    
            //返回锁
            return true;
    
        }
    
        /**
         * 释放锁。
         *
         * 注意:在释放锁之前,需要先判断锁是否已经超时,如果已经超时的话,那么锁可能已由其他进程获得,
         * 这时直接执行释放操作会导致把其他进程已获得的锁也释放掉
         *
         * @return mixed
         */
        public function unlock()
        {
            $now = time();
            if ($now < $this->_expires) {
                $this->_redis->del($this->_key);
            }
        }
    }
    

    使用锁:

    //有多个条件生成key
    $locker = new RedisLockHelper([
                __METHOD__,
                'some_conditions...',
                'some conditions...',
    ]);
    $locker->lock();
    
    //业务 code ...
        
    $locker->unlock();
        
    

    或者通过回调函数的形式使用:

    //有多个条件生成key
    $locker = new RedisLockHelper([
        __METHOD__,
        'some_conditions...',
        'some conditions...',
    ]);
    $locker->lock(function () {
        //业务 code ...
    });
    
        
    

    以上,便是一个redis并发锁的实现流程以及php的实现。

    相关文章

      网友评论

        本文标题:Redis并发锁

        本文链接:https://www.haomeiwen.com/subject/ritcdqtx.html