请教一下关于 Redis setnx 锁的使用是否正确?

V1 create 2023-12-20 09:00

想问一下下面这个获取锁的类,是否能在并发上限制,每次只会处理一个请求,或者下面这样用会有什么别的问题吗,感谢~

  • Redis 锁
class RedisLock
{
    const LOCK_NEGTIVE_BLOCK_TIME = 3 * 1000;  // 阻塞3s (单位微秒)
    const LOCK_HOLD_TIME          = 30 * 1000; // 持有锁的时间(单位微秒)
    const RETRY_DELAY             = 100;       // 循环尝试获取锁时的休眠时间(单位微秒)

    //  Redis 锁 Key
    private string $lockKey;

    // Redis 值 Value (在此指代唯一请求ID)
    private string $requestId;

    public function __construct(string $lockKey, string $requestId)
    {
        $this->lockKey   = $lockKey;
        $this->requestId = $requestId;
    }

    /**
     * 获取锁
     *
     * @param int 锁的过期时间
     * @param boolean 是否阻塞获取锁
     * @param int $blockTime
     * @return bool
     */
    public function get(int $expireTime = self::LOCK_HOLD_TIME, bool $isNegtive = true, int $blockTime = self::LOCK_NEGTIVE_BLOCK_TIME)
    {
        if ($isNegtive) {
            $endtime = microtime(true) * 1000 + $blockTime;
            // 尝试获取锁
            while (microtime(true) * 1000 < $endtime) {
                if (Redis::set($this->lockKey, $this->requestId, 'PX', $expireTime, 'NX')) {
                    return true;
                }
                // 如果获取失败,则会短暂休眠,多次重试
                usleep(self::RETRY_DELAY);
            }
            return false;
        } else {
            // setnx 只获取一次锁 
            return (bool) Redis::set($this->lockKey, $this->requestId, 'PX', $expireTime, 'NX');
        }
    }


    /**
     * 保证删除操作是原子性, (确保只有拥有锁的请求可以解锁)
     *
     * @return bool
     */
    public function del()
    {
        // 检查 lockKey 的值是否等于 requestId,如果是,则删除该键,实现解锁
        $luaScript = <<<EOF
local key = KEYS[1]
local value = ARGV[1]
if redis.call("get", key) == value then
    return redis.call("del", key)
else
    return 0
end
EOF;
        return (bool) Redis::eval($luaScript, 1, $this->lockKey, $this->requestId);
    }
}
  • 使用
// 获得锁
$redisLock = new RedisLock('MyLock', 'xxx');
$isok = $redisLock->get();
if(!$isok) {
    return false;
}
try {
    // do somthing
} catch (\Throwable $th) {
    // handle exception
} finally {
    // 释放锁
    $redisLock->del();
}




V2 update 2023-12-20 10:35

  • 听了楼下的大佬分析后,进行调整,增加 run() 方法,结合闭包,锁住需要执行的逻辑

借鉴 Laravel 的使用方式 Cache::lock('foo', 10)->get(function () {});

 /**
     * 在锁的范围内执行给定闭包。
     *
     * @param closure $closure 需要执行的闭包
     * @param int $expireTime 锁的过期时间(单位微秒)
     * @param bool $isNegtive 是否阻塞模式获取锁
     * @param int $blockTime 阻塞时间(单位微秒)
     *
     * @return mixed 闭包的返回值,或者在未获取到锁时返回 false
     */
    public function run(Closure $closure, int $expireTime = self::LOCK_HOLD_TIME, bool $isNegtive = true, int $blockTime = self::LOCK_NEGTIVE_BLOCK_TIME)
    {
        if ($this->get($expireTime, $isNegtive, $blockTime)) {
            try {
                // 在锁内执行闭包。
                return $closure();
            } catch (\Throwable $th) {
                // 处理异常,可以选择重新抛出或记录日志。
                throw $th;
            } finally {
                // 无论如何都要释放锁。
                $this->del();
            }
        }
        return false;
    }

    // 伪静态
    public static function lock(string $key, string $value, Closure $closure, int $expireTime = self::LOCK_HOLD_TIME, bool $isNegtive = true, int $blockTime = self::LOCK_NEGTIVE_BLOCK_TIME)
    {
        $lock = new self($key, $value);
        return $lock->run($closure, $expireTime, $isNegtive, $blockTime);
    }
  • 使用
// test 1
$result = (new RedisLock('requestLock', 'requestKey'))->run(function () {
    echo 'do something...';
    // 如果需要,返回一些结果
    return 'success';
});

// test 2
$result = RedisLock::lock('requestLock', 'requestKey', function () {
    echo 'do something...';
    // 如果需要,返回一些结果
    return 'success';
});
明天我们吃什么 悲哀藏在现实中 Tacks
最佳答案

你可以用guzzlehttp/guzzle 发送请求池试下

锁用起来略显麻烦,可以抄 laravel的改下

laravel: Cache::lock('foo', 10)->get(function () {});

function get(\Closure $fn){
$redisLock = new RedisLock('MyLock', 'xxx');
$isok = $redisLock->get();
$redisLock = new RedisLock('MyLock', 'xxx');
$isok = $redisLock->get();
if(!$isok) {
    return false;
}
try {
 $fn();
} catch (\Throwable $th) {
    // handle exception
} finally {
    // 释放锁
    $redisLock->del();
}
}

get(function(){
  // do somthing
});
4个月前 评论
Tacks (楼主) 4个月前
讨论数量: 12

可以,这个类写得挺好的

4个月前 评论

你可以用guzzlehttp/guzzle 发送请求池试下

锁用起来略显麻烦,可以抄 laravel的改下

laravel: Cache::lock('foo', 10)->get(function () {});

function get(\Closure $fn){
$redisLock = new RedisLock('MyLock', 'xxx');
$isok = $redisLock->get();
$redisLock = new RedisLock('MyLock', 'xxx');
$isok = $redisLock->get();
if(!$isok) {
    return false;
}
try {
 $fn();
} catch (\Throwable $th) {
    // handle exception
} finally {
    // 释放锁
    $redisLock->del();
}
}

get(function(){
  // do somthing
});
4个月前 评论
Tacks (楼主) 4个月前

是否正确取决于对redis锁理解以及如何处理死锁,理解到位了,封装只是顺手的事了,看了楼主的封装,自己顺道也学习了下,看了redis命令手册中如何处理死锁,与楼主的处理基本一致,细微差别就是get获取锁时间判断过期,SETNX 处理死锁

虽然很多框架封装了这些功能包,做到了开箱即用,有时候顺道阅读源码只是过个脑子,很多时候需要自己上手实操,才能加深记忆,赞一下 :+1:

4个月前 评论
Jyunwaa

usleep会阻塞,这种东西还是更适合异步/协程。

4个月前 评论
Tacks (楼主) 4个月前

如果任务未完成但是锁的过期时间已过期怎么办?

1个月前 评论
Tacks (楼主) 1个月前

大时间锁,在执行中挂了呢?锁没有回收怎么办?

1个月前 评论
Tacks (楼主) 1个月前

挂了还能捕获异常??????

1个月前 评论
Tacks (楼主) 1个月前

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