请教一下关于 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 });
2年前 评论
Tacks (楼主) 2年前
讨论数量: 12

可以,这个类写得挺好的

2年前 评论

你可以用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 });
2年前 评论
Tacks (楼主) 2年前

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

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

2年前 评论
Jyunwaa

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

2年前 评论
Tacks (楼主) 2年前

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

2年前 评论
Tacks (楼主) 2年前

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

2年前 评论
Tacks (楼主) 2年前

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

2年前 评论
Tacks (楼主) 2年前