1. 程式人生 > 其它 >如何基於 Redis 實現分散式鎖

如何基於 Redis 實現分散式鎖

什麼是分散式鎖

分散式鎖:不同程序必須以互斥方式使用共享資源的一種鎖方法實現。

實現分散式鎖的基礎

互斥。任何時刻,只有一個客戶端持有鎖。
無死鎖。最終總是有可能獲得鎖,即使持有鎖的客戶端已經崩潰。

單個 Redis 分散式鎖實現

上鎖

上鎖需要考慮倆點

  • 原子性
  • 鎖能自動釋放

首先要考慮持有鎖的客戶端掛掉後,鎖一直得不到釋放的情況,這時候需要藉助 Redis 的 Expire 自動過期功能,在上鎖時設定一個過期時間,鎖到期後自動釋放,就可以避免死鎖的狀況。
但問題來了,現在有個難題,上鎖和設定過期時間是倆個步驟,如果用 Redis的 SETNX+EXPIRE 倆條指令,這不能保證上鎖這一步驟是互斥的。假設現在有個執行緒A 已經執行完 SETNX,正在執行 EXPIRE 指令,Redis 宕機了,這鎖就有概率變成死鎖了。
為了避免死鎖的產生,我們要將上鎖步驟設定為原子性。這裡有倆種方式實現:

  • Lua
  • SET EX NX

使用上述倆種,都能保證上鎖是原子性的,死鎖問題解決。

持有鎖

上鎖時設定了鎖的過期時間。假如鎖已經過期釋放了,但業務還沒執行完,這時候另外一客戶端得到鎖。此時臨界區資源同時被倆程序使用,違背互斥。
為了解決這一問題,持有鎖的客戶端得定期去延長鎖的過期時間。但如果去延期鎖呢?直接 SET KEY EXPIRE 的話,其他程序也可以隨意修改鎖的過期時間,不安全。
所以需要一個能夠一個 UUID 證明是鎖的持有者。在上鎖時,將鎖的 value 設定為該執行緒的 UUID;在延期鎖時,先獲取鎖的 value 對比該執行緒持有的 UUID,一致後才能延期。

釋放鎖

釋放鎖的時候也需要先證明是鎖的持有者,然後再執行 delete 操作。證明+釋放操作是倆步的,我們也要將其變成原子性的,一般使用 Lua 指令碼執行證明+釋放鎖操作。

多 Redis 例項的分散式鎖實現

現在我們已經完成一個對於單個始終可用的 Redis 例項的分散式鎖版本。下面我們來分散式鎖拓展到分散式 Redis 系統中。

我們可以將基於單例項的 Redis 分散式鎖思路應用到多 Redis 環境裡來。在示例中,我們有 5 個 Redis Master 例項,我們需要確保在這些 Redis 例項中鎖資訊是一致的。
為了獲取鎖,客戶端執行以下操作:

  1. 以毫秒為單位獲取當前時間。
  2. 嘗試按順序獲取所有 N 個例項中的鎖,在所有例項中使用相同的鍵名和隨機值。在步驟 2 中,在每個例項中設定鎖時,客戶端以一個比鎖的自動釋放時間更小的超時時間來獲取鎖。例如,如果鎖的自動釋放時間為 10 秒,則獲取鎖的超時時間應該設定為 5 ~ 50 毫秒範圍內。這可以防止客戶端長時間處於阻塞狀態,嘗試與已關閉的 Redis 節點通訊:如果例項不可用,我們應該儘快嘗試與下一個例項通訊。
  3. 客戶端通過從當前時間減去在步驟 1 中獲得的時間戳來計算獲取鎖所經過的時間。當且僅當客戶端能夠在大多數例項(至少 3 個)中獲取鎖,並且獲取鎖所用的總時間小於鎖有效期時,鎖被視為已獲取。
  4. 如果獲取了鎖,則其有效時間被視為初始有效時間減去經過的時間,如步驟 3 中計算的那樣。
  5. 如果客戶端由於某種原因(無法鎖定 N/2+1 個例項或有效時間為負)未能獲取鎖,那麼將嘗試解鎖所有 Redis例項(甚至是客戶端認為無法獲取鎖的例項)

專案實現

以下專案是基於上述思路實現分佈鎖的成功案例,我們在開發過程中可以上手即用,不必自己重複造輪子。

參考連結:

https://redis.io/docs/manual/patterns/distributed-locks/
https://juejin.cn/post/6936956908007850014