具体实现代码
/**
* 要求
* 安全性(互斥性):在任意时刻,当且仅当只有一个客户端能持有锁
*活性 A(无死锁):即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
*同一性:加锁和解锁必须保证为同一个客户端
*活性 B (容错性):只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁
*/
class DistributedLock
{
/**
* @param $lockKey key
* @param $requestId value
* @param $expireTime 过期时间
* @return bool
*/
//错误加锁示例1
public function lock($lockKey, $requestId, $expireTime)
{
$setNx = \Redis::setnx($lockKey, $requestId);
if ($setNx) {
//此处乍一看这种方式并没有什么问题,但是由于是两条 redis 命令,So 不具有原子性,如果程序在执行完第一句 setnx 命令之后突然挂掉,那么会发生死锁,和设计原则相违背。因此不是最优解。
//若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
$this->redis->expire($lockKey, $expireTime);
return true;
}
return false;
}
/**
* @param $lockKey
* @param $requestId
* @param $expireTime
* @return bool
*/
//错误示例2
public function lock2($lockKey, $requestId, $expireTime)
{
$expire = microtime(true) + $expireTime;
//如果当前锁不存在返回加锁成功
$setNx = \Redis::setnx($lockKey, microtime(true));
if ($setNx) {
return true;
}
//如果锁存在,获取锁的过期时间
$currentExpire = \Redis::get($lockKey);
if ($currentExpire && $currentExpire < microtime(true)) {
//说明锁已经过期,获取上一个锁的过期时间,并设置现在的锁的过期时间
$oldExpires = \Redis::getSet($lockKey, $expire);
if ($oldExpires && $oldExpires == $currentExpire) {
//考虑到多线程并发,只有一个线程的设置值和当前值相同才有权利加锁
return true;
}
}
//其他情况,一律加锁失败
return false;
//由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步;
//当锁过期的时候,如果多个客户端同时执行 getset 方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖;
//锁不具备拥有者标识,即任何客户端都可以解锁(即A线程抢的了锁,但是执行速度较慢,锁已经过期.此时线程B抢得了锁.B执行过程中,A线程执行完毕,释放了锁,所以会造成锁混乱)
//因此此锁安全性没法保证,不满足设计原则第一条
}
public function lock3($lockKey, $requestId, $expireTime)
{
$set = \Redis::set($lockKey, $requestId, 'PX', $expireTime, 'NX');
if ($set) {
return true;
}
return false;
}
public function delLock($lockKey, $requestId)
{
$luaScript = <<<EOF
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
EOF;
// 利用lua脚本,保证原子性
$res = \Redis::eval($luaScript, 1, $lockKey, $requestId);
if ($res) {
return true;
}
return false;
}
}
调用示例
//使用实例 $distributedLock = new DistributedLock(); $distributedLock->lock3("test", "test", 1000); if (!$distributedLock) { return "抢占锁失败"; } try { //todo 业务逻辑处理 } catch (\Exception $e) { //todo 异常处理 } finally { $distributedLock->delLock("test","test"); }