天天看點

聊聊如何用 Redis 實作分布式鎖?

作者:小林coding

計算機八股文網站:https://xiaolincoding.com

哈喽,我是小林。

今天跟大家聊聊兩個問題:

  • 如何用 Redis 實作分布式鎖?
  • Redis 是如何解決叢集情況下分布式鎖的可靠性問題的?

如何用 Redis 實作分布式鎖的?

分布式鎖是用于分布式環境下并發控制的一種機制,用于控制某個資源在同一時刻隻能被一個應用所使用。如下圖所示:

聊聊如何用 Redis 實作分布式鎖?

Redis 本身可以被多個用戶端共享通路,正好就是一個共享存儲系統,可以用來儲存分布式鎖,而且 Redis 的讀寫性能高,可以應對高并發的鎖操作場景。

Redis 的 SET 指令有個 NX 參數可以實作「key不存在才插入」,是以可以用它來實作分布式鎖:

  • 如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;
  • 如果 key 存在,則會顯示插入失敗,可以用來表示加鎖失敗。

基于 Redis 節點實作分布式鎖時,對于加鎖操作,我們需要滿足三個條件。

  • 加鎖包括了讀取鎖變量、檢查鎖變量值和設定鎖變量值三個操作,但需要以原子操作的方式完成,是以,我們使用 SET 指令帶上 NX 選項來實作加鎖;
  • 鎖變量需要設定過期時間,以免用戶端拿到鎖後發生異常,導緻鎖一直無法釋放,是以,我們在 SET 指令執行時加上 EX/PX 選項,設定其過期時間;
  • 鎖變量的值需要能區分來自不同用戶端的加鎖操作,以免在釋放鎖時,出現誤釋放操作,是以,我們使用 SET 指令設定鎖變量值時,每個用戶端設定的值是一個唯一值,用于辨別用戶端;

滿足這三個條件的分布式指令如下:

SET lock_key unique_value NX PX 10000 
           
  • lock_key 就是 key 鍵;
  • unique_value 是用戶端生成的唯一的辨別,區分來自不同用戶端的鎖操作;
  • NX 代表隻在 lock_key 不存在時,才對 lock_key 進行設定操作;
  • PX 10000 表示設定 lock_key 的過期時間為 10s,這是為了避免用戶端發生異常而無法釋放鎖。

而解鎖的過程就是将 lock_key 鍵删除(del lock_key),但不能亂删,要保證執行操作的用戶端就是加鎖的用戶端。是以,解鎖的時候,我們要先判斷鎖的 unique_value 是否為加鎖用戶端,是的話,才将 lock_key 鍵删除。

可以看到,解鎖是有兩個操作,這時就需要 Lua 腳本來保證解鎖的原子性,因為 Redis 在執行 Lua 腳本時,可以以原子性的方式執行,保證了鎖釋放操作的原子性。

// 釋放鎖時,先比較 unique_value 是否相等,避免鎖的誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
           

這樣一來,就通過使用 SET 指令和 Lua 腳本在 Redis 單節點上完成了分布式鎖的加鎖和解鎖。

基于 Redis 實作分布式鎖有什麼優缺點?

基于 Redis 實作分布式鎖的優點:

  1. 性能高效(這是選擇緩存實作分布式鎖最核心的出發點)。
  2. 實作友善。很多研發工程師選擇使用 Redis 來實作分布式鎖,很大成分上是因為 Redis 提供了 setnx 方法,實作分布式鎖很友善。
  3. 避免單點故障(因為 Redis 是跨叢集部署的,自然就避免了單點故障)。

基于 Redis 實作分布式鎖的缺點:

  • 逾時時間不好設定。如果鎖的逾時時間設定過長,會影響性能,如果設定的逾時時間過短會保護不到共享資源。比如在有些場景中,一個線程 A 擷取到了鎖之後,由于業務代碼執行時間可能比較長,導緻超過了鎖的逾時時間,自動失效,注意 A 線程沒執行完,後續線程 B 又意外的持有了鎖,意味着可以操作共享資源,那麼兩個線程之間的共享資源就沒辦法進行保護了。
    • 那麼如何合理設定逾時時間呢? 我們可以基于續約的方式設定逾時時間:先給鎖設定一個逾時時間,然後啟動一個守護線程,讓守護線程在一段時間後,重新設定這個鎖的逾時時間。實作方式就是:寫一個守護線程,然後去判斷鎖的情況,當鎖快失效的時候,再次進行續約加鎖,當主線程執行完成後,銷毀續約鎖即可,不過這種方式實作起來相對複雜。
  • Redis 主從複制模式中的資料是異步複制的,這樣導緻分布式鎖的不可靠性。如果在 Redis 主節點擷取到鎖後,在沒有同步到其他節點時,Redis 主節點當機了,此時新的 Redis 主節點依然可以擷取鎖,是以多個應用服務就可以同時擷取到鎖。

Redis 如何解決叢集情況下分布式鎖的可靠性?

為了保證叢集環境下分布式鎖的可靠性,Redis 官方已經設計了一個分布式鎖算法 Redlock(紅鎖)。

它是基于多個 Redis 節點的分布式鎖,即使有節點發生了故障,鎖變量仍然是存在的,用戶端還是可以完成鎖操作。

Redlock 算法的基本思路,是讓用戶端和多個獨立的 Redis 節點依次請求申請加鎖,如果用戶端能夠和半數以上的節點成功地完成加鎖操作,那麼我們就認為,用戶端成功地獲得分布式鎖,否則加鎖失敗。

這樣一來,即使有某個 Redis 節點發生故障,因為鎖的資料在其他節點上也有儲存,是以用戶端仍然可以正常地進行鎖操作,鎖的資料也不會丢失。

Redlock 算法加鎖三個過程:

  • 第一步是,用戶端擷取目前時間。
  • 第二步是,用戶端按順序依次向 N 個 Redis 節點執行加鎖操作:
    • 加鎖操作使用 SET 指令,帶上 NX,EX/PX 選項,以及帶上用戶端的唯一辨別。
    • 如果某個 Redis 節點發生故障了,為了保證在這種情況下,Redlock 算法能夠繼續運作,我們需要給「加鎖操作」設定一個逾時時間(不是對「鎖」設定逾時時間,而是對「加鎖操作」設定逾時時間)。
  • 第三步是,一旦用戶端完成了和所有 Redis 節點的加鎖操作,用戶端就要計算整個加鎖過程的總耗時(t1)。

加鎖成功要同時滿足兩個條件(簡述:如果有超過半數的 Redis 節點成功的擷取到了鎖,并且總耗時沒有超過鎖的有效時間,那麼就是加鎖成功):

  • 條件一:用戶端從超過半數(大于等于 N/2+1)的 Redis 節點上成功擷取到了鎖;
  • 條件二:用戶端擷取鎖的總耗時(t1)沒有超過鎖的有效時間。

加鎖成功後,用戶端需要重新計算這把鎖的有效時間,計算的結果是「鎖的最初有效時間」減去「用戶端為擷取鎖的總耗時(t1)」。

加鎖失敗後,用戶端向所有 Redis 節點發起釋放鎖的操作,釋放鎖的操作和在單節點上釋放鎖的操作一樣,隻要執行釋放鎖的 Lua 腳本就可以了。

系列《圖解Redis》文章:

面試篇:

  • 3 萬字 + 40 張圖 | 攻破 40 道 Redis 常見面試題

資料類型篇:

  • 2 萬字 + 30 張圖 | 細說 Redis 九種資料類型和應用場景
  • 2 萬字 + 40 張圖 | 圖解 Redis 九種資料結構的實作

持久化篇:

  • AOF 持久化是怎麼實作的?
  • RDB 快照是怎麼實作的?

功能篇:

  • Redis 過期删除政策和記憶體淘汰政策有什麼差別?

高可用篇:

  • 主從複制是怎麼實作的?
  • 為什麼要有哨兵?

緩存篇:

  • 什麼是緩存雪崩、擊穿、穿透?
  • 資料庫和緩存如何保證一緻性?

微信搜尋公衆号:「小林coding」 ,回複「圖解」即可免費獲得「圖解網絡、圖解系統、圖解MySQL、圖解Redis」PDF 電子書