# [**分布式锁**](https://baike.baidu.com/item/分布式锁/10459578?fr=aladdin)
分布式锁可以理解为:控制分布式系统`有序`的对`共享资源`进行操作,通过`互斥`来保持一致性。
举例:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推
### **Redis 分布式锁作用:**
Redis 写入时不带锁定功能,为防止多个进程同时进行一个操作,出现意想不到的结果,对插入更新操作时自定义加锁功能。
### **Redis 分布式锁实现原理:**
* 互斥性
保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作
* 安全性
只有加锁的服务才能解锁权限
* 避免死锁
出现死锁就会导致后续的任何服务都拿不到锁
* 保证加锁与解锁操作是原子性操作
*****
在进程请求执行操作前进行判断,加锁是否成功,执行如下操作:
加锁不成功,则判读锁的值(时间戳)(设定一般为加锁的时候时间戳+自定义过期时间)是否大于当前时间,则获取锁失败不允许执行下步操作;
如果锁的值(时间戳)小于当前时间,并且`GETSET`命令获取到的锁旧值依然小于当前时间,则获取锁成功允许执行下步操作;
如果锁的值(时间戳)小于当前时间,并且`GETSET`命令获取到的锁旧值依然大于当前时间,则获取锁失败不允许执行下步操作;
*****
### **$redis->setnx() 设置锁:**
~~~
$expire = 10; // 有效期10秒
$key = 'lock'; // key
$value = time() + $expire; // 锁的值 = Unix时间戳 + 锁的有效期
$lock = $redis->setnx($key, $value);
/**
* $lock
* 如果返回 1,则表示当前进程获得锁,并获得了当前插入/更新缓存的操作权限
* 如果返回 0,表示锁已被其他进程获取,这是进程可以返回结果或者等待当前锁失效再请求。
*/
if (!empty($lock)) {
// 下步操作
}
~~~
### **解决死锁:**
*****
如果只用`SETNX`命令设置锁的话,如果当持有锁的进程崩溃或删除锁失败时,其他进程将无法获取到锁,问题就大了。
解决方法是在获取锁失败的同时获取锁的值,并将值与当前时间进行对比,如果值小于当前时间说明锁以过期失效,进程可运用`Redis`的`DEL`命令删除该锁
*****
~~~
$expire = 10; // 有效期10秒
$key = 'lock'; // key
$value = time() + $expire; // 锁的值 = Unix时间戳 + 锁的有效期
$status = true;
while ($status) {
$lock = $redis->setnx($key, $value);
if (empty($lock)) {
$value = $redis->get($key);
if ($value < time()) {
$redis->del($key); // 删除过期锁
} else {
usleep(1000); // 睡眠等待会儿
}
} else {
$status = false;
// 后续操作
}
}
~~~
*****
但是,简单粗暴的用`DEL`命令删除锁再`SETNX`命令上锁也会出现问题。比如,`进程1`获得锁后崩溃或删除锁失败,这时`进程2`检测到锁存在当已过期,用`DEL`命令删除锁并用`SETNX`命令设置锁,`进程3`也检测到锁过期,也用`DEL`命令删除锁也用`SETNX`命令设置了锁,这时`进程2`和`进程3`同时获得了锁。这就出现问题。
为了解决这个问题,这里用到了`Redis`的`GETSET`命令,`GETSET`命令在给锁设置新值的同时返回锁的旧值,这里利用了`GETSET`命令同时获取和赋值的特性,在此期间其他进程无法修改锁的值。
比如:
`进程1`获得锁后操作超时/崩溃/删除锁失败;
`进程2`检测到锁已存在,但获取锁的值对比当前时间发现锁已过期,
`进程2`通过`GETSET`命令重新给锁赋予新的值,并获取到的锁的旧值,再次对比锁的旧值与当前时间,如果锁的旧值依然小于当前时间的话,这时`进程2`就可以忽略`进程1`余留下的废锁进行下步操作了。
这里要说明的是,如果有其他进程在`进程2`之前获取到锁,那么`进程2`将获取锁失败,但是`进程2`在用`GETSET`获取锁的旧值时也赋予了锁新的值,改写了其他进程赋予锁的超时值。看到这大家可能会有疑问了,`进程2`没获取到锁怎么能改变锁的值呢?是的,`进程2`改变了锁的原有值,但这一点小小的时间误差带来的影响是可以忽略。
*****
### **Redis 分布式锁 PHP具体实现:**
~~~
$key = 'test'; // 要更新信息的缓存 key
$lockKey = 'lock_' . $key; // 设置锁的 key
$lockExpire = 10; // 设置锁的有效时间
$result = $redis->get($key); // 获取缓存信息
if (empty($result)) {
$status = true;
while ($status) {
$lockValue = $lockExpire + time(); // 设置锁的过去时间
/**
* 创建锁
* 以 $lockKey 为 key 值, $lockValue 为 value 值
* 由于 setnx() 函数只有在不存在当前 key 的缓存时才会创建成功
* 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了
*/
$lock = $redis->setnx($lockKey, $lockValue);
/**
* 满足一个条件就可以继续进行操作
* 1 上面创建锁成功
* 2 判断锁的值是否小于当前时间 get() 同时给锁设置新值 getset()
*/
if (!empty($lock)) {
// 给锁设置生存时间,避免死锁
$redis->expire($lockKey, $lockExpire);
/************************
* 此处进行业务处理
* 执行插入、更新缓存操作...
************************
*/
// 业务走完,删除锁
if ($redis->ttl($lockKey)) {
$redis->del($lockKey);
}
$status = false;
} else {
/**
* 如果存在有效锁
* 睡眠会,等前面操作完在继续请求
*/
sleep(1);
}
}
}
~~~
### **Redis 分布式锁 Redis 命令介绍:**
**setnx(key, value)**
将`key`的值设为`value`,当且仅当`key`不存在。
若给定的`key`已经存在,则`SETNX`不做任何动作。
`SETNX`是『SET if Not eXists』(如果不存在,则`SET`)的简写。
返回值:
设置成功,返回`1`。
设置失败,返回`0`。
**get(key)**
返回`key`所关联的字符串值。
如果`key`不存在则返回特殊值`nil`。
假如`key`储存的值不是字符串类型,返回一个错误,因为`GET`只能用于处理字符串值。
返回值:
`key`的值。
如果`key`不存在,返回`nil`。
**getset(key, value)**
将给定`key`的值设为`value`,并返回`key`的旧值。
当`key`存在但不是字符串类型时,返回一个错误。
返回值:
返回给定`key`的旧值`old value`。
当`key`没有旧值时,返回`nil`。
**expire(key, seconds)**
为给定`key`设置生存时间。
当`key`过期时,它会被自动删除。
在`Redis`中,带有生存时间的`key`被称作“易失的”(volatile)。
返回值:
设置成功返回`1`。
当`key`不存在或者不能为`key`设置生存时间时(比如在低于2.1.3中你尝试更新`key`的生存时间),返回`0`。
**ttl(key)**
返回给定`key`的剩余生存时间(time to live)(以秒为单位)。
返回值:
`key`的剩余生存时间(以秒为单位)。
当`key`不存在或没有设置生存时间时,返回`-1` 。
**del(key)**
移除给定的一个或多个`key`。
返回值:
被移除`key`的数量。
## [**类实现参考**](https://www.cnblogs.com/syhx/p/9753433.html)
> 来源 https://www.cnblogs.com/wenxiong/p/3954174.html