请教一下关于 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';
});
你可以用
guzzlehttp/guzzle
发送请求池试下锁用起来略显麻烦,可以抄 laravel的改下
laravel:
Cache::lock('foo', 10)->get(function () {});