Laravel中使用RedisLock,添加入下
try {
$lock = RedisLock::lock($id, 10); // 锁过期时间为10秒
$lock->block(5); // 最多等待5秒
// 业务逻辑
} catch (LockTimeoutException $e) {
// 拿锁超时
abort(400, '拿锁超时');
} finally {
// 释放锁
if (isset($lock)) {
$lock->release();
}
}
有一次redis服务异常,恢复后,发现某些访问某些$id的页面,一直拿锁超时错误,导致页面不可用
经过分析,发现是部分锁没有正常释放导致,RedisLock部分代码如下:
public static function lock($name, $seconds, $owner = null, $redis = null)
{
$driver = config('database.redis-driver');
if ($driver === 'ckv' || is_null($redis)) {
$redis = Redis::connection();
}
return new self($name, $seconds, $owner, $redis);
}
public function block($seconds, $callback = null)
{
$starting = $this->currentTime();
while (!$this->acquire()) {
usleep(250 * 1000);
if ($this->currentTime() - $seconds >= $starting) {
throw new LockTimeoutException();
}
}
if (is_callable($callback)) {
try {
return $callback();
} finally {
$this->release();
}
}
return true;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
$result = $this->redis->setnx($this->name, $this->owner); // 问题就出现在这里!!!!!!设置key
if ($result === 1 && $this->seconds > 0) {
$this->redis->expire($this->name, $this->seconds); // 设置超时时间
}
return $result === 1;
}
看到没有,上面acquire方法里setnx和expire是两步调用,这样使得锁的原子性得不到满足,导致redis异常时,设置了key,但是expire执行失败
redis已经有支持设置key和expire的原子操作,将这里修改下就ok了
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
if ($this->seconds > 0) {
$result = $this->redis->set($this->name, $this->owner, 'EX', $this->seconds, 'NX');
return $result == 'OK';
} else {
$result = $this->redis->setnx($this->name, $this->owner);
return $result === 1;
}
}