天天看点

记一次 Redis分布式锁 使用遇到的问题

Laravel中使用RedisLock,添加入下

try {
            $lock = RedisLock::lock($id, 10); // 锁过期时间为10秒
            $lock->block(5); // 最多等待5秒

            // 业务逻辑
        } catch (LockTimeoutException $e) {
            // 拿锁超时
            abort(400, '拿锁超时');
        } finally {
            // 释放锁
            if (isset($lock)) {
                $lock->release();
            }
        }
           

有一次redis服务异常,恢复后,发现某些访问某些$id的页面,一直拿锁超时错误,导致页面不可用

经过分析,发现是部分锁没有正常释放导致,RedisLock部分代码如下:

public static function lock($name, $seconds, $owner = null, $redis = null)
    {
        $driver = config('database.redis-driver');
        if ($driver === 'ckv' || is_null($redis)) {
            $redis = Redis::connection();
        }
        return new self($name, $seconds, $owner, $redis);
    }

	public function block($seconds, $callback = null)
    {
        $starting = $this->currentTime();
        while (!$this->acquire()) {
            usleep(250 * 1000);
            if ($this->currentTime() - $seconds >= $starting) {
                throw new LockTimeoutException();
            }
        }
        if (is_callable($callback)) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }
        return true;
    }

    /**
     * Attempt to acquire the lock.
     *
     * @return bool
     */
    public function acquire()
    {
        $result = $this->redis->setnx($this->name, $this->owner); // 问题就出现在这里!!!!!!设置key
        if ($result === 1 && $this->seconds > 0) {
            $this->redis->expire($this->name, $this->seconds);  // 设置超时时间
        }
        return $result === 1;
    }
           

看到没有,上面acquire方法里setnx和expire是两步调用,这样使得锁的原子性得不到满足,导致redis异常时,设置了key,但是expire执行失败

redis已经有支持设置key和expire的原子操作,将这里修改下就ok了

/**
     * Attempt to acquire the lock.
     *
     * @return bool
     */
    public function acquire()
    {
        if ($this->seconds > 0) {
            $result = $this->redis->set($this->name, $this->owner, 'EX', $this->seconds, 'NX');
            return $result == 'OK';
        } else {
            $result = $this->redis->setnx($this->name, $this->owner);
            return $result === 1;
        }
    }