代码改变世界

Laravel Redis分布式锁实现源码分析

2020-07-03 18:09  小伍2013  阅读(776)  评论(0编辑  收藏  举报

首先是锁的抽象类,定义了继承的类必须实现加锁、释放锁、返回锁拥有者的方法。

namespace Illuminate\Cache;

abstract class Lock implements LockContract
{
    use InteractsWithTime;

    // 锁的名称
    protected $name;

    // 锁的时长
    protected $seconds;

    // 当前操作锁的拥有者
    protected $owner;

    // 获取锁失败时,重新获取锁需要等待的毫秒数
    protected $sleepMilliseconds = 250;

    // 构造函数
    public function __construct($name, $seconds, $owner = null)
    {
        if (is_null($owner)) {
            $owner = Str::random();
        }

        $this->name = $name;
        $this->owner = $owner;
        $this->seconds = $seconds;
    }

    // 加锁
    abstract public function acquire();

    // 释放锁
    abstract public function release();

    // 获取锁中保存的拥有者信息
    abstract protected function getCurrentOwner();

    // 1. 尝试获取锁,并返回获取结果
    // 2. 尝试获取锁,获取成功后执行一个回调函数,执行完成后自动释放锁
    public function get($callback = null)
    {
        $result = $this->acquire();

        if ($result && is_callable($callback)) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }

        return $result;
    }

    // 尝试在指定的时间内获取锁,超时则失败抛出异常
    public function block($seconds, $callback = null)
    {
        $starting = $this->currentTime();

        while (! $this->acquire()) {
            usleep($this->sleepMilliseconds * 1000);

            if ($this->currentTime() - $seconds >= $starting) {
                throw new LockTimeoutException;
            }
        }

        if (is_callable($callback)) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }

        return true;
    }

    // 返回当前操作锁的拥有者
    public function owner()
    {
        return $this->owner;
    }

    // 判断当前操作的拥有者是否为锁中保存的拥有者
    protected function isOwnedByCurrentProcess()
    {
        return $this->getCurrentOwner() === $this->owner;
    }

    // 设置重试获取锁需要等待的毫秒数
    public function betweenBlockedAttemptsSleepFor($milliseconds)
    {
        $this->sleepMilliseconds = $milliseconds;
        return $this;
    }
}

Redis 锁实现类,增加了强制删除锁的方法。

class RedisLock extends Lock
{
    // Redis对象
    protected $redis;

    // 构造函数
    public function __construct($redis, $name, $seconds, $owner = null)
    {
        parent::__construct($name, $seconds, $owner);
        $this->redis = $redis;
    }

    // 加锁逻辑代码
    public function acquire()
    {
        if ($this->seconds > 0) {
            return $this->redis->set($this->name, $this->owner, 'EX', $this->seconds, 'NX') == true;
        } else {
            return $this->redis->setnx($this->name, $this->owner) === 1;
        }
    }

    // 使用 Lua 脚本释放锁逻辑代码
    public function release()
    {
        return (bool) $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
    }

    // 无视锁的拥有者强制删除锁
    public function forceRelease()
    {
        $this->redis->del($this->name);
    }

    // 返回锁中保存的拥有者信息
    protected function getCurrentOwner()
    {
        return $this->redis->get($this->name);
    }
}

原子性释放锁的 Lua 脚本。

class LuaScripts
{
    /**
     * 使用 Lua 脚本原子性地释放锁.
     *
     * KEYS[1] - 锁的名称
     * ARGV[1] - 锁的拥有者,只有是该锁的拥有者才允许释放
     *
     * @return string
     */
    public static function releaseLock()
    {
        return <<<'LUA'
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
LUA;
    }
}

总结:

  1. 可以通过get()方法直接获取锁并传入回调函数在成功时执行。
  2. 可以通过block()方法在指定时间内不断获取锁,知道成功或超时为止,成功时会执行传入的回调函数。
  3. Redis 通过 set() 命令设置一个值为“拥有者”的字符串来作为锁。
  4. set() 通过 NX 参数来实现排他锁(只在键不存在时,才对键进行设置)。
  5. set() 通过 EX 参数来控制锁的生存时间(防止程序意外终止发生死锁)。
  6. 不能使用 set()+expire() 来代替set(),防止网络延迟或其他故障导致死锁。
  7. Redis 通过 Lua 脚本来达到原子性删除锁。
  8. Lua 脚本中会判断字符串的内容是否与参数中的拥有者一致,一致才执行删除操作。防止当前锁被其他进程误删除,或者误删除了其他进程的锁。