深入理解分佈式鎖——以 Redis 爲例
一、分佈式鎖簡介
1. 什麼是分佈式鎖
分佈式鎖是一種在分佈式系統環境下,通過多個節點對共享資源進行訪問控制的一種同步機制。它的主要目的是防止多個節點同時操作同一份數據,從而避免數據的不一致性。
-
線程鎖:
也被稱爲互斥鎖(
Mutex
),主要用於控制同一進程中的多個線程對共享資源的訪問。 -
進程鎖
進程鎖是用於控制同一臺機器上的多個進程對共享資源的訪問。進程鎖可以是系統級的,如文件鎖,也可以是用戶級的,如信號量(
Semaphore
)。 -
分佈式鎖
分佈式鎖是用於控制分佈式系統中的多個節點對共享資源的訪問。由於分佈式系統中的節點可能位於不同的機器甚至不同的地理位置,因此分佈式鎖的實現比線程鎖和進程鎖要複雜得多。分佈式鎖需要在網絡中的多個節點之間進行協調,以保證鎖的唯一性和一致性。
2. 分佈式鎖的特性
分佈式鎖主要有以下幾個特性:
-
互斥性:在任何時刻,只有一個節點可以持有鎖。
-
不會發生死鎖:如果一個節點崩潰,鎖可以被其他節點獲取。
-
公平性:如果多個節點同時申請鎖,系統應該保證每個節點都有獲取鎖的機會。
-
可重入性:同一個節點可以多次獲取同一個鎖,而不會被阻塞。
-
高可用:鎖服務應該是高可用的,不能因爲鎖服務的故障而影響整個系統的運行。
二、分佈式鎖的基本原理
1. 分佈式鎖的基本步驟
分佈式鎖的基本原理可以分爲以下幾個步驟:
-
請求鎖:當一個實例需要訪問共享資源時,它會向分佈式鎖系統發送一個請求,試圖獲取一個鎖。
-
鎖定資源:分佈式鎖系統會檢查是否有其他實例已經持有這個鎖。如果沒有,那麼這個實例就會獲得鎖,並且有權訪問共享資源。如果有,那麼這個實例就必須等待,直到鎖被釋放。
-
訪問資源:一旦實例獲取了鎖,它就可以安全地訪問共享資源,而不用擔心其他實例會同時訪問這個資源。
-
釋放鎖:當實例完成對共享資源的訪問後,它需要通知分佈式鎖系統釋放鎖。這樣,其他正在等待的實例就可以獲取鎖,訪問共享資源。
2. 分佈式鎖實現的關鍵點
在實現分佈式鎖時,通常會有一箇中心節點(或者稱爲鎖服務),所有需要獲取鎖的節點都需要向這個中心節點申請。
當一個節點申請鎖時,中心節點會檢查當前是否有其他節點持有鎖,如果沒有,則將鎖分配給申請的節點;如果有,則拒絕申請。當持有鎖的節點完成操作後,會向中心節點歸還鎖,此時其他的節點可以再次申請鎖。
三、基於 Redis 的分佈式鎖
1. Redis 的基本介紹
Redis 是一個開源的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息代理。
Redis 提供了多種命令和能力來支持實現分佈式鎖
-
SETNX
命令:SETNX
(Set if Not Exists)命令用於在 key 不存在時設置值。這是實現分佈式鎖的關鍵命令,因爲它能確保在同一時間只有一個客戶端能夠獲得鎖。 -
EXPIRE
命令:EXPIRE
命令用於爲 key 設置過期時間。這對於避免死鎖非常重要,因爲即使某個客戶端崩潰,鎖也會在一定時間後自動釋放。 -
DEL
命令:DEL
命令用於刪除 key。在釋放鎖時,需要使用此命令刪除對應的 key。 -
Lua 腳本:Redis 支持使用 Lua 腳本來執行一系列原子操作。這對於實現安全的分佈式鎖非常有用,因爲它可以確保在釋放鎖時檢查鎖的持有者。
-
RedLock 算法:Redis 官方推薦了一種名爲 RedLock 的分佈式鎖算法。RedLock 是一種基於多個 Redis 實例的分佈式鎖算法,旨在提供更高的安全性和容錯能力。
一般,在實現 Redis 分佈式鎖時,不分開使用 SETNX 和 EXPIRE 命令,而是使用 SETNX 的拓展命令 SET NX EX
示例:
SET my_key my_value NX EX 10 # 設置鍵值對, 超時時間爲10s。 如果my_key存在,則不進行任何操作
2. Redis 實現分佈式鎖的基本實現
-
請求鎖
假設我們有一個 Redis 鍵
my_lock
,用於表示鎖的狀態。當一個客戶端想要獲取鎖時,它會嘗試使用SETNX
命令來設置這個鍵。SET my_lock<unique_value> NX EX <lock_timeout>
如果命令返回
OK
,則表示客戶端成功獲取了鎖。如果返回nil
,則表示鎖已被其他客戶端持有。 -
<unique_value>
: 一個唯一的值,比如 UUID,用於標識鎖的持有者。 -
NX
: 只有當my_lock
不存在時,纔會設置該鍵。這確保了同一時間只有一個客戶端能獲得鎖。 -
EX <lock_timeout>
: 設置鎖的過期時間,防止因客戶端崩潰而導致的死鎖。 -
鎖續期
爲了防止鎖過早地因爲過期而被釋放,可以在鎖快到期時進行續期操作。這可以通過定期檢查鎖的剩餘時間,並在必要時使用
EXPIRE
命令來更新過期時間來實現。
# 檢查鎖是否仍由當前客戶端持有
if redis.call("get", "my_lock") ==<unique_value>" then
# 續期鎖
redis.call("EXPIRE", "my_lock", <new_lock_timeout>)
end
注意:上述代碼是一個簡化的 Lua 腳本示例,實際應用中可能需要更復雜的邏輯來處理續期操作。
-
釋放鎖
當客戶端完成需要加鎖保護的操作後,應該釋放鎖。爲了確保只有鎖的持有者才能釋放鎖,可以使用 Lua 腳本來執行釋放操作。
if redis.call("get", "my_lock") ==<unique_value>" then
return redis.call("del", "my_lock")
else
return 0 -- 鎖未被當前客戶端持有,無法釋放
end
這個 Lua 腳本首先檢查鎖是否仍由當前客戶端持有,如果是,則刪除 `my_lock` 鍵以釋放鎖。
3. Redis 分佈式鎖的使用場景
Redis 分佈式鎖可以用於所有需要在分佈式環境中同步訪問共享資源的場景。例如,電商秒殺活動中,爲了防止超賣,可以使用 Redis 分佈式鎖來保證同一時刻只有一個請求可以操作庫存。又如,在分佈式計算中,爲了防止重複計算,可以使用 Redis 分佈式鎖來保證同一時刻只有一個節點可以進行計算。
4. Redis 分佈式鎖的優點和缺點
優點:
-
性能高:由於 Redis 是基於內存的,因此 Redis 分佈式鎖的性能非常高。
-
實現簡單:Redis 提供的命令可以很容易地實現分佈式鎖。
缺點:
-
不可重入:Redis 分佈式鎖默認是不可重入的,如果需要可重入,需要額外的邏輯來實現。
-
非阻塞:Redis 分佈式鎖是非阻塞的,如果獲取鎖失敗,需要自己進行重試。
-
安全性:如果 Redis 服務器出現故障,可能會導致鎖無法正常工作。
四、其他分佈式鎖的實現方式
1. 基於數據庫的分佈式鎖
數據庫分佈式鎖是通過在數據庫中創建一個鎖表,表中包含鎖的名稱和鎖的狀態等信息。
當一個節點需要獲取鎖時,它會在這個表中插入一條記錄,如果插入成功,那麼這個節點就獲取到了鎖。當節點使用完鎖後,會刪除這條記錄,從而釋放鎖。
這種方式的優點是實現簡單,缺點是性能較低,且如果數據庫出現故障,可能會影響到鎖的功能。
2. 基於 Zookeeper 的分佈式鎖
Zookeeper 是一個開源的分佈式協調服務,它提供了一種高效且可靠的分佈式鎖實現方式。
在 Zookeeper 中,可以創建一個臨時節點作爲鎖,當一個節點需要獲取鎖時,它會嘗試創建這個臨時節點,如果創建成功,那麼這個節點就獲取到了鎖。
當節點使用完鎖後,會刪除這個臨時節點,從而釋放鎖。如果節點崩潰,Zookeeper 會自動刪除這個臨時節點,從而避免了死鎖的問題。
3. 基於 Etcd 的分佈式鎖
Etcd 是一個開源的分佈式鍵值存儲系統,它也提供了一種分佈式鎖的實現方式。
Etcd 的分佈式鎖是通過創建一個帶有 TTL(Time To Live)的鍵值對來實現的,當一個節點需要獲取鎖時,它會嘗試創建這個鍵值對,如果創建成功,那麼這個節點就獲取到了鎖。
當節點使用完鎖後,會刪除這個鍵值對,從而釋放鎖。如果節點崩潰,Etcd 會自動刪除這個鍵值對,從而避免了死鎖的問題。
4. 各種實現方式的比較
在選擇分佈式鎖的實現方式時,需要根據具體的應用場景和需求來決定。
五、分佈式鎖的常見問題和解決方案
1. 死鎖問題
-
問題:
當一個客戶端獲取了鎖,但由於某些原因(如程序崩潰、異常等)無法釋放鎖時,會導致其他客戶端永遠無法獲取鎖。
-
解決方案:
設置鎖的過期時間。當鎖的持有者未能在過期時間內執行完畢並釋放鎖時,鎖將自動過期,從而允許其他客戶端獲取鎖。
2. 鎖續命問題
-
問題:
如果一個操作需要的時間可能超過鎖的過期時間,那麼在操作執行過程中鎖過期會導致其他客戶端獲取到鎖,從而產生併發問題。
解決方案:
使用鎖續命機制。在鎖持有者執行操作期間,可以定期檢查鎖是否即將過期,並在適當的時候對鎖進行續命,即重新設置鎖的過期時間。
3. 鎖釋放問題
-
問題:
爲確保數據的一致性,只有鎖的持有者才能釋放鎖。但在實際應用中,可能會出現誤解鎖的情況。
-
解決方案:
在設置鎖時,爲鎖關聯一個唯一的值(如 UUID)。在釋放鎖時,先檢查鎖的值是否與當前客戶端的值匹配,如果匹配則釋放鎖,否則不做任何操作。注意,鎖持有人的判斷和鎖的釋放應該在一個原子操作內完成。
4. 鎖的公平性問題
-
問題:
在高併發環境中,如果多個節點同時請求獲取鎖,可能會出現 “飢餓” 現象,即某些節點長時間無法獲取到鎖。
-
解決方案:
引入隊列,將請求鎖的節點按照順序排隊。例如,在 Zookeeper 中,可以使用順序節點來實現公平鎖。
5. 鎖的可重入性問題
-
問題:
在某些場景中,一個節點可能需要多次獲取同一個鎖,如果鎖不支持重入,可能會導致死鎖。
-
解決方案:
爲鎖添加一個擁有者的概念,只有鎖的擁有者才能再次獲取到鎖。例如,在 Redis 中,可以將鎖的值設置爲節點的唯一標識,獲取鎖時檢查鎖的值是否爲自己的標識。
6. 鎖的安全性問題
-
問題:
如果分佈式鎖的存儲系統(如 Redis、Zookeeper 等)出現故障,可能會導致鎖無法正常工作。
-
解決方案:
使用高可用的存儲系統,如使用 Redis 集羣或 Zookeeper 集羣。另外,可以使用心跳機制來檢測存儲系統的狀態,如果檢測到故障,可以及時進行切換。
六、參考文件
- Distributed Locks with Redis
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tmX0foZ0t7y4NpN6du_Dmw