简单了解redis分布式锁

redis分布式锁

项目中的分布式锁代码

我们先来观察一个分布式锁的代码

加锁

    public function addExclusiveLock($lockName, int $lockExpire = 3) {
        $lockName = 'exclusivelock:' . $lockName;
        $retryTimes = 0;
        $maxRetryTimes = $lockExpire > 10 ? 10 : $lockExpire;
        while (false === ($addResult = $this->client->setnx($lockName, 1)) && $retryTimes < $maxRetryTimes) {
            $retryTimes++;
            sleep(1);
        }
        if ($addResult) {
            $this->client->expire($lockName, $lockExpire);
            return true;
        }
        return false;
    }

加锁操作

  1. 重复尝试加锁,若失败达重试上则返回false
  2. 加锁成功设置过期时间,以防锁不能正常释放

解锁

    public function removeExclusiveLock($lockName) {
        $lockName = 'exclusivelock:' . $lockName;
        $this->client->del($lockName);
    }

解锁操作 : 直接删除锁

以上代码存在的问题

死锁

在加锁之后,设置过期之前程序异常退出,会造成死锁。原因就是因为 setnx 和 expire 是两条指令而不是一条原子指令。

   public function addExclusiveLock($lockName, int $lockExpire = 3) {
       $lockName = 'exclusivelock:' . $lockName;
       $retryTimes = 0;
       $maxRetryTimes = $lockExpire > 10 ? 10 : $lockExpire;
       while (false === ($addResult = $this->client->setnx($lockName, 1)) && $retryTimes < $maxRetryTimes) {
           $retryTimes++;
           sleep(1);
       }
       // 此处异常退出
       if ($addResult) {
           $this->client->expire($lockName, $lockExpire);
           return true;
       }
       return false;
   }
  • 解决方法
    ①Lua脚本
    ②Redis2.6以后可以使用set解决
       ///set exclusivelock:test 1  nx ex  5
       $this->client->set($lockName, 1, ['NX','EX' => $lockExpire]);
    
偷锁

假设

  1. 线程A各种原因要执行8s,执行到5s时,锁过期了
  2. 此时线程B可以“正常”获取到锁并加锁
  3. 时间来到第八秒,线程A终于执行完了,进行del(这时候删的是B的锁)
  4. 线程 B执行完懵了,我锁呢?(此时有可能线程C申请到锁,然后B又把C的锁删了)

原因就是加锁时,没有对各线程之间的锁就行区分
解决方案 之一:

  1. 将锁的value设置为线程id(或者其他保证区别的东西)
  2. 在del之前,比对当前线程id与锁中记录的id
    public function removeExclusiveLock($lockName, $requestId) {
        $lockName = 'exclusivelock:' . $lockName;
        if ($requestId == $this->client->get($lockName)){
            $this->client->del($lockName);
        }
    }

但上面的代码由于get和del是两条指令而不是一条原子指令。(这句话好熟悉)
理论上存在如下情况。

偷锁2.0
  1. 在线程A解锁时,运行到注释行忽然锁过期了
  2. 线程 B加锁成功
  3. 线程A接着执行,就把线程B的锁删了(是有点苛刻)
    public function removeExclusiveLock($lockName, $requestId) {
        $lockName = 'exclusivelock:' . $lockName;
        if ($requestId == $this->client->get($lockName)){
        	//若在此时,这把锁突然不是这个客户端的,则会误解锁
            $this->client->del($lockName);
        }
    }

解决原子性:使用Lua脚本

    public function removeExclusiveLock($lockName, $requestId) {
        $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        $lockName = 'exclusivelock:' . $lockName;
        $result = $this->client->eval($script, [$lockName, $requestId], 1);
        return $result == 1;
    }

其他

单节点的redis分布式锁用这套方案足够了,如果需要更高的一致性以及分布式锁主从同步的问题可以了解Redlock以及ZooKeeper

参考

  • https://www.cnblogs.com/shamo89/p/8931197.html
  • http://zhangtielei.com/posts/blog-redlock-reasoning.html
  • https://juejin.im/book/6844733724618129422/section/6844733724702015495
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值