# 使用Redis來進行分散式鎖
[TOC]
透過上一篇[好像要鎖一下ㄟ(悲觀鎖、樂觀鎖)](https://hackmd.io/@UTRxSLfpRa6ds1oeI2U7Lw/H1SFq3CFs)中提到了透過DB的方式來解決賣超的方法,今天就來記錄一下如果不用DB那要怎麼解決哩?
## 悲觀鎖
- 上次使用的是悲觀鎖,底下的原理是透過資料庫行鎖來達成,這樣的話會有以下這些困擾的問題:
1. 效能問題,在資料庫層面會一直阻塞,直到commit
2. 注意設定事務的隔離級別是`Read Commit`,否則併發情況下,另外的事物無法看到提交的資料,導致賣超問題
3. 容易產生deadlock,如果多個加鎖控制不好,就會容易發生
## 簡單介紹分散式鎖
- 我們可以透過Redis、ZooKeeper來實現
- 特點
1. `互斥性`。在任意時刻,只有一個人持有鎖
2. `鎖超時`。即使一個人持有鎖的期間發生異常,而沒有主動釋放鎖,也需要保證其他人能正常獲取鎖
3. 獲得鎖和解鎖必須是`同一個人`,不能把別人的鎖給釋放了
- 這樣的話我們就可以不需要使用悲觀鎖,來解決賣超、一致性等問題
## 分散式鎖的觸發流程
```mermaid
graph TD;
A拿到鎖-->A做某些事情;
A做某些事情-->A解鎖;
A解鎖-->換下一個人拿鎖;
換下一個人拿鎖-->A拿到鎖;
```
- 從圖中我們可以知道分散式鎖的`獲得鎖和解鎖必須是同一個人`,一次只給一個人拿到鎖,同一時間不會有其他人拿到相同的鎖
## 實作
- 透過Redis來實作分散式鎖,同時有10個人搶同樣的東西
### 拿到鎖
- `acquire_timeout`:每個人能獲得鎖的上限時間為`30s`
- `lock_timeout`:鎖的過期時間為`10s`
- 其中`acquire_timeout`必須==大於== `lock_timeout`,因為如果==小於==的話就不符合上面提到的`鎖超時`了
```python=
def acquire_lock(lock_name, p, acquire_timeout=30, lock_timeout=10):
"""
獲取鎖
:param lock_name:
:param p:
:param acquire_timeout: 每個人能獲得鎖的上限時間
:param lock_timeout: 鎖的過期時間
:return:
"""
identifier = str(uuid.uuid4())
# 設定當前用戶在特定時間內一定要拿到鎖,若超過acquire_timeout時間都還沒拿到鎖,則return False
end = time.time() + acquire_timeout
while time.time() < end:
# nx: 如果不存在才創建、ex: 過期時間
# 設置鎖的過期時間,防止deadlock,並返回uuid當作唯一值
if r.set(lock_name, identifier, ex=lock_timeout, nx=True):
print(f"進程{p} 獲得鎖")
return identifier
return False
```
### 解鎖
- 當下的redis儲存的uuid必須跟當下的user是同樣的才能正常解鎖 => `同一人才能解鎖`
```python=
def release_lock(lock_name, identifier):
unlock_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
unlock = r.register_script(unlock_script)
result = unlock(keys=[lock_name], args=[identifier])
if result:
return True
else:
return False
```
### 執行結果
- 我們可以看到一次只會有一個進程獲得鎖和解鎖,不會有A進程獲得鎖、B進行解鎖的情況
```python=
if __name__ == '__main__':
for i in range(10):
Process(target=exec_test, args=("lock:test", i)).start()
# 進程2 獲得鎖
# 876146d3-0502-43a5-a93c-d136e1281623
# 進程2 成功解鎖
# 進程4 獲得鎖
# b3682b7c-07e6-41b9-af2a-50f189ed77c2
# 進程4 成功解鎖
# 進程3 獲得鎖
# e292c337-4e51-48d3-bc4c-a98445628942
# 進程3 成功解鎖
# 進程5 獲得鎖
# 6a820501-04a8-4263-815a-34633722fc5a
# 進程5 成功解鎖
# 進程6 獲得鎖
# 8a0fdc15-35d9-45c5-9421-c2b988b736d9
# 進程6 成功解鎖
# 進程0 獲得鎖
# 4abb0888-0624-401f-8c92-971792246f0b
# 進程0 成功解鎖
# 進程7 獲得鎖
# 5231363d-6ee4-47a7-9546-deecbe51a8a3
# 進程7 成功解鎖
# 進程8 獲得鎖
# e177da52-67fb-4a6c-8fe4-271f53a126f2
# 進程8 成功解鎖
# 進程1 獲得鎖
# 25279d1e-6b09-46c9-b8da-6e2f37143a1b
# 進程1 成功解鎖
# 進程9 獲得鎖
# 133dca70-3bda-4942-ac78-ea3ee9f39bbd
# 進程9 成功解鎖
```
### 結論
寫法上應該還有其他種,原理是相同的。另外`Zookeeper`也可以實作出分散式鎖,看別人的分享在實作上是比Redis更簡易的,有興趣的可以自己玩玩看~
## 參考資料
1. [隔離級別介紹](https://openhome.cc/Gossip/HibernateGossip/IsolationLevel.html)
2. https://www.gushiciku.cn/pl/ggMl/zh-tw
3. https://www.cnblogs.com/tracydzf/p/15629987.html
4. [redis、zookeeper面試介紹](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-lock-redis-vs-zookeeper.md)