# [**分布式鎖**](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