「Redis」redis 分佈式鎖

不積跬步,無以至千里;不積小流,無以成江海。

分佈式鎖

分佈式鎖 是控制 分佈式系統 之間 同步 訪問 共享資源 的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間 共享 了一個或一組資源,那麼訪問這些資源的時候,往往需要 互斥 來防止彼此干擾來保證 一致性,在這種情況下,便需要使用到分佈式鎖。

簡單一點:分佈式鎖的本質其實就是先佔一個 “坑”位,當別的進程進來也要持有時,發現已經被佔用,而不得不放棄。

搶鎖一般是使用 setnx (set if not exists) 指令,只允許被一個客戶端搶鎖。先來先佔, 用完了,再調用 del 指令釋放鎖。

<code>> setnx 

lock

:key 

true

OK ... 

do

 something ... > del 

lock

:key (integer) 

1

/<code>

但是有個問題,如果邏輯執行到中間出現異常了,可能會導致 del 指令沒有被調用,這樣就會陷入 死鎖

,鎖永遠得不到釋放。

於是我們在拿到鎖之後,再給鎖加上一個 過期時間,比如 5s,這樣即使中間出現異常也可以保證 5 秒之後鎖會自動釋放。

<code>> setnx 

lock

:key 

true

OK > expire 

lock

:key 

5

... 

do

 something  ... > del 

lock

:key (integer) 

1

/<code>

仔細想一下,以上邏輯還有問題。如果在 setnx 和 expire 之間服務器進程突然掛掉了,可能是因為機器掉電或者是被人為殺掉的,就會導致 expire 得不到執行,也會造成死鎖。

這種問題的根源就在於 setnx 和 expire 是兩條指令而不是 原子 指令(Wiki 解釋:所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch 線程切換。)

如果這兩條指令可以一起執行就不會出現問題。也許你會想到用 Redis 事務來解決。但是這裡不行,因為 expire 是依賴於 setnx 的執行結果的,如果 setnx 沒搶到鎖,expire 是不應該執行的。事務裡沒有 if-else 分支邏輯,事務的特點是一口氣執行,要麼全部執行要麼一個都不執行。

Redis 2.8 以上版本中作者加入了 set 指令的擴展參數,使得 setnx 和 expire 指令可以一起執行,徹底解決了分佈式死鎖的問題。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :設置鍵的過期時間為second秒
  • PX millisecond :設置鍵的過期時間為millisecond毫秒
  • NX :只在鍵不存在時,才對鍵進行設置操作
  • XX:只在鍵已經存在時,才對鍵進行設置操作
  • SET操作成功完成時,返回OK ,否則返回nil
<code>> 

set

 

lock

:key 

true

 ex 

5

 nx OK ... 

do

 something  ... > del 

lock

:key/<code>

指令 setnx 和 expire 組合在一起的原子指令,它就是單節點分佈式鎖的奧義所在。

鎖超時問題

如下執行順序:

  1. 客戶端1獲取鎖成功。
  2. 客戶端1在某個操作上 阻塞 了很長時間。
  3. 過期時間到了,鎖 自動 釋放了。
  4. 客戶端2獲取到了對應同一個資源的鎖。
  5. 客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。

之後,客戶端2在訪問共享資源的時候,就沒有鎖為它提供保護了。

這時 為 key 的 value 設置一個 隨機數 很有必要的,釋放鎖時先匹配隨機數是否一致,然後再刪除 key,它保證了 一個客戶端 釋放的鎖必須是 自己 持有的那個鎖。

切記:釋放鎖的操作 必須 使用 Lua腳本 來實現。因為釋放鎖其實包含三步操作:’GET’、判斷 和 ’DEL’,用Lua腳本來實現能保證這三步的 原子性。否則,如果把這三步操作放到客戶端邏輯中去執行的話,還有可能發生以上問題。

  1. 客戶端1獲取鎖成功。
  2. 客戶端1訪問共享資源。
  3. 客戶端1為了釋放鎖,先執行’GET’操作獲取 隨機字符串 的值。
  4. 客戶端1判斷隨機字符串的值,與預期的值相等。
  5. 客戶端1由於某個原因阻塞住了很長時間。
  6. 過期時間到了,鎖自動釋放了。
  7. 客戶端2獲取到了對應同一個資源的鎖。
  8. 客戶端1從阻塞中恢復過來,執行DEL操縱,釋放掉了客戶端2持有的鎖。

實際上,如果不是客戶端被阻塞住了,而是出現了大的 網絡延遲,也有可能導致類似的執行序列發生。

這樣使用也只是相對安全一點,現實中還要從自己的業務出發,Redis 分佈式鎖不要用於較長時間的任務。如果真的偶爾出現了,數據出現的小波錯亂可能需要人工介入解決。

Redlock 算法

假如Redis節點 宕機 了,那麼 所有 客戶端就都無法獲得鎖了,服務變得不可用。為了提高 可用性,我們可以給這個Redis節點掛一個Slave,當Master節點不可用的時候,系統自動切到Slave上(failover)。但由於Redis的 主從複製(replication)是異步的,這可能導致在failover過程中喪失鎖的安全性。考慮下面的執行序列:

  1. 客戶端1從Master獲取了鎖。
  2. Master宕機了,存儲鎖的key還沒有來得及同步到Slave上。
  3. Slave升級為Master。
  4. 客戶端2從新的Master獲取到了對應同一個資源的鎖。

於是,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破。針對這個問題,antirez設計了 Redlock算法

為了使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關係。同很多分佈式算法一樣,redlock 也使用「大多數機制」。

加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx)

指令,只要 過半 節點 set 成功,那就認為加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。不過 Redlock 算法還需要考慮出錯重試、時鐘漂移等很多細節問題,同時因為 Redlock 需要向多個節點進行讀寫,意味著相比單實例 Redis 性能會下降一些。

如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,那就應該考慮 redlock。不過代價也是有的,需要更多的 redis 實例,性能也下降了,這些都是需要考慮的成本,使用前還請想清楚自己想要什麼。

點關注 不迷路

以上就是這篇的全部內容,能看到這裡的都是 人才。

如果你從本篇內容有收穫,求 點贊,求 關注,求 轉發 ,讓更多的人學習到。

如果本文有任何錯誤,請批評指教,不勝感激


分享到:


相關文章: