redis分布式锁
项目中的分布式锁代码
我们先来观察一个分布式锁的代码
加锁
public function addExclusiveLock($lockName, int $lockExpire = 3) {
$lockName = 'exclusivelock:' . $lockName;
$retryTimes = 0;
$maxRetryTimes = $lockExpire > 10 ? 10 : $lockExpire;
while (false === ($addResult = $this->client->setnx($lockName, 1)) && $retryTimes < $maxRetryTimes) {
$retryTimes++;
sleep(1);
}
if ($addResult) {
$this->client->expire($lockName, $lockExpire);
return true;
}
return false;
}
加锁操作
- 重复尝试加锁,若失败达重试上则返回false
- 加锁成功设置过期时间,以防锁不能正常释放
解锁
public function removeExclusiveLock($lockName) {
$lockName = 'exclusivelock:' . $lockName;
$this->client->del($lockName);
}
解锁操作 : 直接删除锁
以上代码存在的问题
死锁
在加锁之后,设置过期之前程序异常退出,会造成死锁。原因就是因为 setnx 和 expire 是两条指令而不是一条原子指令。
public function addExclusiveLock($lockName, int $lockExpire = 3) {
$lockName = 'exclusivelock:' . $lockName;
$retryTimes = 0;
$maxRetryTimes = $lockExpire > 10 ? 10 : $lockExpire;
while (false === ($addResult = $this->client->setnx($lockName, 1)) && $retryTimes < $maxRetryTimes) {
$retryTimes++;
sleep(1);
}
// 此处异常退出
if ($addResult) {
$this->client->expire($lockName, $lockExpire);
return true;
}
return false;
}
- 解决方法
①Lua脚本
②Redis2.6以后可以使用set解决///set exclusivelock:test 1 nx ex 5 $this->client->set($lockName, 1, ['NX','EX' => $lockExpire]);
偷锁
假设
- 线程A各种原因要执行8s,执行到5s时,锁过期了
- 此时线程B可以“正常”获取到锁并加锁
- 时间来到第八秒,线程A终于执行完了,进行del(这时候删的是B的锁)
- 线程 B执行完懵了,我锁呢?(此时有可能线程C申请到锁,然后B又把C的锁删了)
原因就是加锁时,没有对各线程之间的锁就行区分
解决方案 之一:
- 将锁的value设置为线程id(或者其他保证区别的东西)
- 在del之前,比对当前线程id与锁中记录的id
public function removeExclusiveLock($lockName, $requestId) {
$lockName = 'exclusivelock:' . $lockName;
if ($requestId == $this->client->get($lockName)){
$this->client->del($lockName);
}
}
但上面的代码由于get和del是两条指令而不是一条原子指令。(这句话好熟悉)
理论上存在如下情况。
偷锁2.0
- 在线程A解锁时,运行到注释行忽然锁过期了
- 线程 B加锁成功
- 线程A接着执行,就把线程B的锁删了(是有点苛刻)
public function removeExclusiveLock($lockName, $requestId) {
$lockName = 'exclusivelock:' . $lockName;
if ($requestId == $this->client->get($lockName)){
//若在此时,这把锁突然不是这个客户端的,则会误解锁
$this->client->del($lockName);
}
}
解决原子性:使用Lua脚本
public function removeExclusiveLock($lockName, $requestId) {
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$lockName = 'exclusivelock:' . $lockName;
$result = $this->client->eval($script, [$lockName, $requestId], 1);
return $result == 1;
}
其他
单节点的redis分布式锁用这套方案足够了,如果需要更高的一致性以及分布式锁主从同步的问题可以了解Redlock以及ZooKeeper
参考
- https://www.cnblogs.com/shamo89/p/8931197.html
- http://zhangtielei.com/posts/blog-redlock-reasoning.html
- https://juejin.im/book/6844733724618129422/section/6844733724702015495