解锁 Redis 锁的正确姿势

解锁redis锁的正确姿势

redis是php的好朋友,在php写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为。这个时候我们就要用到锁。锁的方式有好几种,php不能在内存中用锁,不能使用zookeeper加锁,使用数据库做锁又消耗比较大,这个时候我们一般会选用redis做锁机制。

setnx

锁在redis中最简单的数据结构就是string。最早的时候,上锁的操作一般使用setnx,这个命令是当:lock不存在的时候set一个val,或许你还会记得使用expire来增加锁的过期,解锁操作就是使用del命令,伪代码如下:

if (Redis::setnx("my:lock", 1)) {
    Redis::expire("my:lock", 10);
    // ... do something

    Redis::del("my:lock")
}

这里其实是有问题的,问题就在于setnx和expire中间如果遇到crash等行为,可能这个lock就不会被释放了。于是进一步的优化方案可能是在lock中存储timestamp。判断timestamp的长短。

set

现在官方建议直接使用set来实现锁。我们可以使用set命令来替代setnx,就是下面这个样子

if (Redis::set("my:lock", 1, "nx", "ex", 10)) {
    ... do something

    Redis::del("my:lock")
}

上面的代码把my:lock设置为1,当且仅当这个lock不存在的时候,设置完成之后设置过期时间为10。

获取锁的机制是对了,但是删除锁的机制直接使用del是不对的。因为有可能导致误删别人的锁的情况。

比如,这个锁我上了10s,但是我处理的时间比10s更长,到了10s,这个锁自动过期了,被别人取走了,并且对它重新上锁了。那么这个时候,我再调用Redis::del就是删除别人建立的锁了。

官方对解锁的命令也有建议,建议使用lua脚本,先进行get,再进行del

程序变成:


$token = rand(1, 100000);

function lock() {
    return Redis::set("my:lock", $token, "nx", "ex", 10);
}

function unlock() {
    $script = `
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end    
    `
    return Redis::eval($script, "my:lock", $token)
}

if (lock()) {
    // do something

    unlock();
}

这里的token是一个随机数,当lock的时候,往redis的my:lock中存的是这个token,unlock的时候,先get一下lock中的token,如果和我要删除的token是一致的,说明这个锁是之前我set的,否则的话,说明这个锁已经过期,是别人set的,我就不应该对它进行任何操作。

所以:不要再使用setnx,直接使用set进行锁实现。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 7年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 20
$token = rand(1,1000000)
while (true)
 {
    if(redisLock($token))
    {
         //业务内容
         unlock($token);
         break;
    }
 }

function redisLock($token)
{
   return Redis::set("my:lock", $token, "ex", "5", "nx");
}

function unlock($token)
{
    $script = "if redis.call('get',KEYS[1]) == ARGV[1] 
    then return redis.call('del',KEYS[1]) 
    else return 0 
    end";
    return Redis::eval($script,1,'my:lock',$token);
}
4年前 评论

楼主,问下啊,如果我想锁定对同一个资源的访问,像您这样锁不同的 token,这种方式好像不太合适吧。

7年前 评论

使用lua脚本跟直接用php实现有什么区别么

7年前 评论

@tradzero lua脚本是原子的

7年前 评论

从博客园搬过来常驻了吗?这么高产 :joy:

7年前 评论

存在一个问题,有可能 rand 的时候,“自己”和“别人” rand 出相同的结果了,所以最好是使用类似于处境 id,uuid 作为 token。

7年前 评论

有一个问题哈,如果先获得锁的用户a在过期时间之内没有执行完操作,用户b获得了锁,那a b的操作会有冲突的可能吧

6年前 评论
13697332484 3年前
szmrzhou

解锁的时候出现ERR value is not an integer or out of range,这个问题怎么解?好像是调用redis::eval出现的

5年前 评论

@张小张 锁资源通过 key,不是value

5年前 评论

@szmrzhou eval($script, "1", "my:lock", $token),少个1

5年前 评论

这样写对吗?你们用过没

5年前 评论
13697332484 3年前
$token = rand(1,1000000)
while (true)
 {
    if(redisLock($token))
    {
         //业务内容
         unlock($token);
         break;
    }
 }

function redisLock($token)
{
   return Redis::set("my:lock", $token, "ex", "5", "nx");
}

function unlock($token)
{
    $script = "if redis.call('get',KEYS[1]) == ARGV[1] 
    then return redis.call('del',KEYS[1]) 
    else return 0 
    end";
    return Redis::eval($script,1,'my:lock',$token);
}
4年前 评论

非常有用赞👍

4年前 评论
drinke9

楼主的的代码在我这边执行不成功。以下是我这边执行成功的代码。laravel 版本 5.8 phpredis

    /**
     * Redis 加锁
     * @param $bar_id
     * @param $token
     * @return mixed
     */
    private function lockBar($bar_id, $token)
    {
        $key = "barId:".$bar_id;

        return Redis::connection('bar')->set($key, true, "EX", 10, "NX");
    }

    /**
     * Redis 解锁
     * @param $bar_id
     * @param $token integer
     * @return mixed
     */
    private function unlockBar($bar_id, int $token)
    {
        $script = <<<'LUA'
if redis.call('get', KEYS[1]) == ARGV[1]
then
    return redis.call('del', KEYS[1])
else
    return 0
end
LUA;

        return Redis::connection('bar')->eval($script, 1, "barId:".$bar_id, $token);
    }
4年前 评论
xuanjiang 3年前

$x = Redis::set("test", 1, "nx", "ex", 10) ; var_dump($x); $y = Redis::set("test", 1, "nx", "ex", 10) ; var_dump($y); die;

2个都输出 true ?? 怎么加锁的 是版本问题吗

3年前 评论

我想问一下,你们发现文章里的set参数和评论里的set参数不一样吗,顺序不对,有知道的朋友指点一下吗

2年前 评论

评论的才对的,楼主的set代码错了。。这么核心的代码,差点搞崩系统

1周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!