分布式鎖其實就是,控制分布式系統不同進程共同訪問共享資源的一種鎖的實現。如果不同的系統或同一個系統的不同主機之間共享了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
本篇內容包括:關于 Redis 與 分布式鎖,Redis 分布式鎖的問題及解決方式,Redis 中的 Lua 腳本 以及 Redis 中的 RedLock 算法!
文章目錄
- 一、關于 Redis 與 分布式鎖
- 1、關于分布式鎖
- 2、關于 Redis 實現分布式鎖
- 二、Redis 分布式鎖的問題及解決方式
- 三、Redis 中的 Lua 腳本
- 四、Redis 中的 RedLock 算法
- 1、Redis 中的 RedLock 算法
- 2、 Redlock 算法的客戶端的執行步驟
一、關于 Redis 與 分布式鎖
1、關于分布式鎖
在一個分布式系統中,當一個線程去讀取數據并修改的時候,因為讀取和更新保存不是一個原子操作,在并發時就很容易遇到并發問題,進而導致數據的不正確。這種場景很常見,比如電商秒殺活動,庫存數量的更新就會遇到。如果是單機應用,直接使用本地鎖就可以避免。如果是分布式應用,本地鎖派不上用場,這時就需要引入分布式鎖來解決。
一般來說,實現分布式鎖的方式有以下幾種:
- 使用 MySQL,基于唯一索引。
- 使用 ZooKeeper,基于臨時有序節點。
- 使用 Redis,基于 setnx 命令。
2、關于 Redis 實現分布式鎖
Redis 實現分布式鎖主要利用 Redis 的setnx
命令。setnx 是 SET if not exists(如果不存在,則 SET)的簡寫。
加鎖:使用setnx key value
命令,如果 key 不存在,設置 value(加鎖成功)。如果已經存在 lock(也就是有客戶端持有鎖了),則設置失敗(加鎖失敗)
解鎖:使用 del
命令,通過刪除鍵值釋放鎖。釋放鎖之后,其他客戶端可以通過 setnx
命令進行加鎖。
Key 的值可以根據業務設置,比如是用戶中心使用的,可以命令為USER_REDIS_LOCK
,value 可以使用 uuid 保證唯一,用于標識加鎖的客戶端。保證加鎖和解鎖都是同一個客戶端。
二、Redis 分布式鎖的問題及解決方式
首先,有一個致命問題,就是某個線程在獲取鎖之后由于某些異常因素(比如宕機)而不能正常的執行解鎖操作,那么這個鎖就永遠釋放不掉了。為此,我們可以為這個鎖加上一個超時時間為此,我們可以為這個鎖加上一個超時時間
- 執行
SET key value EX seconds
的效果等同于執行SETEX key seconds value
- 執行
SET key value PX milliseconds
的效果等同于執行PSETEX key milliseconds value
然后,此時依然會有問題,某線程 A 獲取了鎖并且設置了過期時間為 10s,然后在執行業務邏輯的時候耗費了 15s,此時線程 A 獲取的鎖早已被 Redis 的過期機制自動釋放了在線程A獲取鎖并經過 10s 之后,改鎖可能已經被其它線程獲取到了。當線程 A 執行完業務邏輯準備解鎖(DEL key
)的時候,有可能刪除掉的是其它線程已經獲取到的鎖。當解鎖時,也就是刪除 key 的時候先判斷一下 key 對應的 value 是否等于先前設置的值,如果相等才能刪除 key。
最后,這里我們還是一眼就可以看出問題來:GET
和DEL
是兩個分開的操作,在 GET 執行之后且在 DEL 執行之前的間隙是可能會發生異常的,我們引入了一種新的方式,就是 Lua 腳本,解決原子性的問題。Redis 會將整個 Lua 腳本作為一個整體執行,中間不會被其他請求插入。
另外,為了防止多個線程同時執行業務代碼,需要確保過期時間大于業務執行時間,可以在代碼增加一個線程用于刷新定時過期時間,并增加一個 bool 類型的值表示是否開啟定時刷新過期時間,在線程獲取鎖的時候,將其設置為 true,解鎖前設置回 false。比如,Redisson 實現,獲取鎖成功就會開啟一個定時任務,定時任務會定期檢查去續期。
此外,還有一個問題:在集群中,主節點掛掉時,從節點會取而代之,客戶端上卻并沒有明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,但是這把鎖還沒有來得及同步到從節點,主節點突然掛掉了。然后從節點變成了主節點,這個新的節點內部沒有這個鎖,所以當另一個客戶端過來請求加鎖時,立即就批準了。這樣就會導致系統中同樣一把鎖被兩個客戶端同時持有,不安全性由此產生。此處可以用 RedLock 算法解決。
三、Redis 中的 Lua 腳本
Lua 是一種輕量小巧的腳本語言,用標準 C 語言編寫并以源代碼形式開放。其設計目的就是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能
Redis 在 2.6 版本推出了 lua 腳本功能,允許開發者使用 Lua 語言編寫腳本傳到 Redis 中執行。
使用 Lua 腳本的好處:
- 原子操作。Redis 會將整個腳本作為一個整體執行,中間不會被其他請求插入。因此在腳本運行過程中無需擔心會出現競態條件,無需使用事務;
- 減少網絡開銷。可以將多個請求通過腳本的形式一次發送,減少網絡時延;
- 復用。客戶端發送的腳本會永久存在 Redis 中,這樣其他客戶端可以復用這一腳本,而不需要使用代碼完成相同的邏輯。
四、Redis 中的 RedLock 算法
1、Redis 中的 RedLock 算法
在集群中,主節點掛掉時,從節點會取而代之,客戶端上卻并沒有明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,但是這把鎖還沒有來得及同步到從節點,主節點突然掛掉了。然后從節點變成了主節點,這個新的節點內部沒有這個鎖,所以當另一個客戶端過來請求加鎖時,立即就批準了。這樣就會導致系統中同樣一把鎖被兩個客戶端同時持有,不安全性由此產生
Redlock 算法就是為了解決這個問題
使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關系。同很多分布式算法一樣,Redlock 也使用大多數機制
加鎖時,它會向過半節點發送 set 指令,只要過半節點 set
成功,那就認為加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。不過 Redlock 算法還需要考慮出錯重試、時鐘漂移等很多細節問題,同時因為 Redlock
需要向多個節點進行讀寫,意味著相比單實例 Redis 性能會下降一些
Redlock 算法是在單 Redis 節點基礎上引入的高可用模式,Redlock 基于 N 個完全獨立的 Redis 節點,一般是大于 3 的奇數個(通常情況下 N 可以設置為 5),可以基本保證集群內各個節點不會同時宕機。
2、 Redlock 算法的客戶端的執行步驟
當 Redis 集群有 5 個節點,運行 Redlock 算法的客戶端的執行步驟:
- 客戶端記錄當前系統時間,以毫秒為單位;
- 依次嘗試從 5 個 Redis 實例中,使用相同的 key 獲取鎖,當向 Redis 請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,超時時間應該小于鎖的失效時間,避免因為網絡故障出現的問題;
- 客戶端使用當前時間減去開始獲取鎖時間就得到了獲取鎖使用的時間,當且僅當從半數以上的 Redis 節點獲取到鎖,并且當使用的時間小于鎖失效時間時,鎖才算獲取成功;
- 如果獲取到了鎖,key 的真正有效時間等于有效時間減去獲取鎖所使用的時間,減少超時的幾率;
- 如果獲取鎖失敗,客戶端應該在所有的 Redis 實例上進行解鎖,即使是上一步操作請求失敗的節點,防止因為服務端響應消息丟失,但是實際數據添加成功導致的不一致。