分布式锁一般用 setnx(set if not exists)
命令,只允许被一个客户端获得锁,先到先得,用完后再用 del
释放锁。
版本1:
# 获取锁
127.0.0.1:6379> setnx lock:temp random
(integer) 1
# ---- 如果得到锁,执行业务代码 ----
# ---- 如果没有得到锁,继续尝试获取锁 ----
# 释放锁
127.0.0.1:6379> del lock:temp
(integer) 1
这样就会有一个问题,如果业务代码执行出错没有被捕捉到,或者由于某种特殊情况程序中断,就会导致 del
命令没有执行到,这样就会陷入死锁,锁永远得不到释放。
版本2:
# 获取锁
127.0.0.1:6379> setnx lock:temp random
(integer) 1
# 设置锁的过期时间
127.0.0.1:6379> expire lock:temp 5
(integer) 1
# ---- 如果得到锁,执行业务代码 ----
# ---- 如果没有得到锁,继续尝试获取锁 ----
# 释放锁
127.0.0.1:6379> del lock:temp
(integer) 1
版本 2 增加了锁的过期时间,这样出现异常没有正常释放锁的话可以保证一断时间后自动释放锁。但版本 2 还是有一个问题,如果在 setnx
和 expire
之间服务器出现异常,导致 expire
命令没有执行到,也会出现死锁问题。因为 setnx
和 expire
是两条分开执行的指令,不是原子操作。
还有一个问题,如果 setnx
没有拿到锁,expire
是不应该执行的。
版本3:
# 获取锁
127.0.0.1:6379> set lock:temp random ex 5 nx
OK
# ---- 如果得到锁,执行业务代码 ----
# ---- 如果没有得到锁,继续尝试获取锁 ----
# 释放锁
127.0.0.1:6379> del lock:temp
(integer) 1
版本 3 使用新的命令 set {key} {value} ex {seconds} nx
解决操作原子性问题。该命令能保证获取锁与设置超时时间操作的原子性。
但是该版本还有问题:
- 如果 A 请求业务代码执行时间超过锁的时间,这时锁会自动释放,这时假如 B 得到锁,B 请求刚得到锁的时候,A 请求的业务代码执行完了,A 请求调用了
del
释放锁,这样就会出问题,因为 A 释放的不是自己的锁。 - 如果业务代码出现异常没有被捕获,程序运行就中止了,这样就会导致锁得不到释放,所以在执行业务代码的时候要增加 try catch。
版本4:
```shell获取锁
127.0.0.1:6379> set lock:temp random ex 5 nx OK
—— 如果得到锁,执行业务代码,注意要捕获业务代码的异常 ——
—— 如果没有得到锁,继续尝试获取锁 ——
先获取存入的随机数
127.0.0.1:6379> get lock:temp “random”
情况1:get 命令获取的随机数与存入的随机数一致,说明当前请求没有超时,调用 del 释放锁
127.0.0.1:6379> del lock:temp (integer) 1
情况2:get 命令获取的随机数与存入的随机数不一致,说明当前请求业务代码执行时长超过锁有效时长,
锁已经提前自动释放,所以不用调用 del
<a name="ocvSP"></a>
## PHP 分布式锁代码实现:
```php
/**
* 分布式锁模板方法,如果获取不到锁则再次尝试
* @param string $key 锁的对象
* @param Closure $handle 回调方法(业务方法)
* @return mixed 业务方法返回值
* @throws Exception
*/
static public function distributedLockTemplate($key, Closure $handle)
{
// 锁有效时长
$time = 5;
// 存入随机数,防止业务代码超时释放锁出错
$random = GUIDService::getGuid();
// 获取锁
$count = self::setExNx($key, $random, $time);
// 记录返回值
$result = null;
// 如果获取到锁
if ($count > 0) {
// 执行业务代码
try {
$result = $handle();
} catch (Exception $exception) {
self::releaseLock($key, $random);
throw $exception;
}
self::releaseLock($key, $random);
} else {
// 如果没有获取到锁,再次尝试获取
sleep(0.2);
// 递归调用
$result = self::distributedLockTemplate($key, $handle);
}
return $result;
}
/**
* 释放锁
* @param $key
* @param $random
*/
static private function releaseLock($key, $random)
{
// 获取随机数,防止业务代码超时释放锁出错
$catchRandom = self::get($key);
// 对比随机数
if ($random == $catchRandom) {
// 释放锁
RedisService::del($key);
}
}
static public function get($key)
{
return Redis::get($key);
}
static public function setExNx($key, $value, $second)
{
return Redis::set($key, $value, 'ex', $second, 'nx');
}
- 获取不到锁的多种处理方式
- 递归调用重新获取
- 抛异常给用户,提示重试
- 放得任务队列重试