最近有朋友问如何使用Redis的SETNX设置一个并发锁(针对的是同一个用户在同一点进行一次行为操作),他发了一段代码,我们看看问题;
先来看下php 中setNx这个函数的介绍:
Set the string value in argument as value of the key if the key doesn't already exist in the database.
如果键在数据库中不存在,则将参数中的字符串值设置为键的值
setNX,是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回 1 , 设置失败时返回 0 。可以利用它来实现锁的效果,但是很多人在使用的过程中都有一些问题没有考虑到。
如下是朋友写的代码:
看起来是没什么问题,假设如果在//更新资金日志里面程序中断了,del是不是就执行不到了,是不是会导致死锁,虽然下面加了一个$redis->expire($key, $ttl);但是问题还是没有解决,没有意义;所以这种做法不是特别可靠;
还有的朋友直接这样做:
$rs = $redis->setNX($key, $value);
redis->expire($key, $ttl);
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁
$redis->del($key);
}
这样显然有一个问题, setNX 成功了 Expire 却失败了是不是又死锁了?
那么朋友说这样呢:
$redis->multi();
$rs = $redis->setNX($key, $value);
redis->expire($key, $ttl);
$redis->exec();
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁
$redis->del($key);
}
为了确保请求的原子性,使用 Multi/Exec确保2次都成功、或者失败;这样还有问题:当多个请求到达时,虽然只有一个请求的 setNX 可以成功,但是任何一个请求的 Expire 却都可以成功,这就意味着即便获取不到锁也可以刷新过期时间,导致锁一直有效,还是解决不了上面的问题。
那么我们如何使用setnx设置一个可靠有效的锁?下面代码实现
/**
* @param $uid
* @param int $timeout
* @return bool
* 设置锁
*/
public function set_mutex($uid,$timeout = 2){
$cur_time = time();
$read_news_mutex_key = "redis:mutex:{$uid}";
$mutex_res = $this->setnx($read_news_mutex_key,$cur_time + $timeout);
if($mutex_res){
return true;
}
//就算意外退出,下次进来也会检查key,防止死锁
$time = $this->get($read_news_mutex_key);
if($cur_time > $time){
$this->del($read_news_mutex_key);
return $this->setnx($read_news_mutex_key,$cur_time + $timeout);
}
return false;
}
/**
* @param $uid
* 释放锁
*/
public function del_mutex($uid){
$read_news_mutex_key = "redis:mutex:{$uid}";
$this->del($read_news_mutex_key);
}
调用如下:
if(!$this->set_mutex($uid)){
$this->error('请求太频繁,请稍后再试',-10001,'','',true);
}
//....代码逻辑
$this->redis->del_mutex($uid);
以上就是正确使用Redis的SETNX设置一个并发锁;
这是不是会存在一个问题,就是大家同用一个锁,锁的粒度太大呢
可以加一个来源用来来区分不同的接口