不積跬步,無以至千里;不積小流,無以成江海。
分佈式鎖
分佈式鎖 是控制 分佈式系統 之間 同步 訪問 共享資源 的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間 共享 了一個或一組資源,那麼訪問這些資源的時候,往往需要 互斥 來防止彼此干擾來保證 一致性,在這種情況下,便需要使用到分佈式鎖。
簡單一點:分佈式鎖的本質其實就是先佔一個 “坑”位,當別的進程進來也要持有時,發現已經被佔用,而不得不放棄。
搶鎖一般是使用 setnx (set if not exists) 指令,只允許被一個客戶端搶鎖。先來先佔, 用完了,再調用 del 指令釋放鎖。
<code>> setnxlock
:keytrue
OK ...do
something ... > dellock
:key (integer)1
/<code>
但是有個問題,如果邏輯執行到中間出現異常了,可能會導致 del 指令沒有被調用,這樣就會陷入 死鎖
,鎖永遠得不到釋放。於是我們在拿到鎖之後,再給鎖加上一個 過期時間,比如 5s,這樣即使中間出現異常也可以保證 5 秒之後鎖會自動釋放。
<code>> setnxlock
:keytrue
OK > expirelock
:key5
...do
something ... > dellock
: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
:keytrue
ex5
nx OK ...do
something ... > dellock
:key/<code>
指令 setnx 和 expire 組合在一起的原子指令,它就是單節點分佈式鎖的奧義所在。
鎖超時問題
如下執行順序:
- 客戶端1獲取鎖成功。
- 客戶端1在某個操作上 阻塞 了很長時間。
- 過期時間到了,鎖 自動 釋放了。
- 客戶端2獲取到了對應同一個資源的鎖。
- 客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。
之後,客戶端2在訪問共享資源的時候,就沒有鎖為它提供保護了。
這時 為 key 的 value 設置一個 隨機數 很有必要的,釋放鎖時先匹配隨機數是否一致,然後再刪除 key,它保證了 一個客戶端 釋放的鎖必須是 自己 持有的那個鎖。
切記:釋放鎖的操作 必須 使用 Lua腳本 來實現。因為釋放鎖其實包含三步操作:’GET’、判斷 和 ’DEL’,用Lua腳本來實現能保證這三步的 原子性。否則,如果把這三步操作放到客戶端邏輯中去執行的話,還有可能發生以上問題。
- 客戶端1獲取鎖成功。
- 客戶端1訪問共享資源。
- 客戶端1為了釋放鎖,先執行’GET’操作獲取 隨機字符串 的值。
- 客戶端1判斷隨機字符串的值,與預期的值相等。
- 客戶端1由於某個原因阻塞住了很長時間。
- 過期時間到了,鎖自動釋放了。
- 客戶端2獲取到了對應同一個資源的鎖。
- 客戶端1從阻塞中恢復過來,執行DEL操縱,釋放掉了客戶端2持有的鎖。
實際上,如果不是客戶端被阻塞住了,而是出現了大的 網絡延遲,也有可能導致類似的執行序列發生。
這樣使用也只是相對安全一點,現實中還要從自己的業務出發,Redis 分佈式鎖不要用於較長時間的任務。如果真的偶爾出現了,數據出現的小波錯亂可能需要人工介入解決。
Redlock 算法
假如Redis節點 宕機 了,那麼 所有 客戶端就都無法獲得鎖了,服務變得不可用。為了提高 可用性,我們可以給這個Redis節點掛一個Slave,當Master節點不可用的時候,系統自動切到Slave上(failover)。但由於Redis的 主從複製(replication)是異步的,這可能導致在failover過程中喪失鎖的安全性。考慮下面的執行序列:
- 客戶端1從Master獲取了鎖。
- Master宕機了,存儲鎖的key還沒有來得及同步到Slave上。
- Slave升級為Master。
- 客戶端2從新的Master獲取到了對應同一個資源的鎖。
於是,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破。針對這個問題,antirez設計了 Redlock算法
為了使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關係。同很多分佈式算法一樣,redlock 也使用「大多數機制」。
加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx)
指令,只要 過半 節點 set 成功,那就認為加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。不過 Redlock 算法還需要考慮出錯重試、時鐘漂移等很多細節問題,同時因為 Redlock 需要向多個節點進行讀寫,意味著相比單實例 Redis 性能會下降一些。如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,那就應該考慮 redlock。不過代價也是有的,需要更多的 redis 實例,性能也下降了,這些都是需要考慮的成本,使用前還請想清楚自己想要什麼。
點關注 不迷路
以上就是這篇的全部內容,能看到這裡的都是 人才。
如果你從本篇內容有收穫,求 點贊,求 關注,求 轉發 ,讓更多的人學習到。
如果本文有任何錯誤,請批評指教,不勝感激 !