鎖是咱們在設計和實現大多數系統時繞不過的話題。一旦有競爭條件出現,在沒有保護的操做的前提下,可能會出現不可預知的問題。redis
而現代系統大多爲分佈式系統,這就引入了分佈式鎖,要求具備在分佈各處的服務上保護資源的能力。數據庫
而實現分佈式鎖,目前大多有如下三種方式:緩存
其中 Redis 簡便靈活,高可用分佈式,且支持持久化。本文即介紹基於 Redis 實現分佈式鎖。bash
使用 Redis 實現分佈式鎖,根本原理是 SETNX 指令。其語義以下:dom
SETNX key value
複製代碼
命令執行時,若是
key
不存在,則設置key
值爲value
(同set
);若是key
已經存在,則不執行賦值操做。並使用不一樣的返回值標識。命令描述文檔分佈式
還能夠經過 SET 命令的 NX 選項使用:ui
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
複製代碼
NX - 僅在 key 不存在時執行賦值操做。命令描述文檔 而以下文所述,經過SET的NX選項使用,可同時使用其它選項,如EX/PX設置超時時間,是更好的方式。lua
下面咱們對比下幾種具體實現方式。spa
僞代碼以下:設計
setnx lock_a random_value
// do sth
delete lock_a
複製代碼
此實現方式的問題在於:一旦服務獲取鎖以後,因某種緣由掛掉,則鎖一直沒法自動釋放。從而致使死鎖。
僞代碼以下:
setnx lock_a random_value
setex lock_a 10 random_value // 10s超時
// do sth
delete lock_a
複製代碼
按需設置超時時間。此方案解決了方案1死鎖的問題,但同時引入了新的死鎖問題: 若是setnx以後,setex 以前服務掛掉,會陷入死鎖。 根本緣由爲 setnx/setex 分爲了兩個步驟,非原子操做。
僞代碼以下:
SET lock_a random_value NX PX 10000 // 10s超時
// do sth
delete lock_a
複製代碼
此方案經過 set 的 NX/PX 選項,將加鎖、設置超時兩個步驟合併爲一個原子操做,從而解決方案一、2的問題。(PX與EX選項的語義相同,差別僅在單位。) 此方案目前大多數 sdk、redis 部署方案都支持,所以是推薦使用的方式。 但此方案也有以下問題:
若是鎖被錯誤的釋放(如超時),或被錯誤的搶佔,或因redis問題等致使鎖丟失,沒法很快的感知到。
方案4在3的基礎上,增長對 value 的檢查,只解除本身加的鎖。 相似於 CAS,不過是 compare-and-delete。 此方案 redis 原生命令不支持,爲保證原子性,須要經過lua腳本實現:。
僞代碼以下:
SET lock_a random_value NX PX 10000
// do sth
eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_a random_value
複製代碼
此方案更嚴謹:即便由於某些異常致使鎖被錯誤的搶佔,也能部分保證鎖的正確釋放。而且在釋放鎖時能檢測到鎖是否被錯誤搶佔、錯誤釋放,從而進行特殊處理。
從上述描述可看出,超時時間是一個比較重要的變量:
超時時間不能過短,不然在任務執行完成前就自動釋放了鎖,致使資源暴露在鎖保護以外。 超時時間不能太長,不然會致使意外死鎖後長時間的等待。除非人爲接入處理。 所以建議是根據任務內容,合理衡量超時時間,將超時時間設置爲任務內容的幾倍便可。 若是實在沒法肯定而又要求比較嚴格,能夠採用按期 setex/expire 更新超時時間實現。
若是拿不到鎖,建議根據任務性質、業務形式進行輪詢等待。 等待次數須要參考任務執行時間。
setnx 使用更爲靈活方案。multi/exec 的事務實現形式更爲複雜。 且部分redis集羣方案(如codis),不支持multi/exec 事務。