本文重點並不在於提供一個可運行的Redis分佈式鎖示例,而是結合圖文理解redis的分佈式鎖實現上的細節,以及爲何要這樣作。redis
用僞代碼的形式簡單介紹實現方式安全
SET resource_name my_random_value NX EX 30
複製代碼
經過lua腳本實現原子的 比較 & 刪除bash
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
複製代碼
let randStr = Math.random();
let success = redis.set(key, randStr, 'EX', 30, 'NX');
if(success){
// 獲取到鎖
doSomething(); //執行要作的任務
//執行完釋放鎖
redis.call(` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`,key,randStr);
}else{
//未獲取到鎖
}
複製代碼
這樣看起來,Redis實現分佈式鎖用起來很簡單嘛。服務器
可是,爲啥這樣寫?dom
先回答後一個問題,釋放鎖以前爲何要先判斷值相等呢,爲何不直接一句del key
多方便啊。分佈式
首先要知道,分佈式鎖一個最基本的功能就是實現互斥,同一個時刻,應當只有一個客戶端可以持有鎖。ui
若是直接del key
,則可能會出現下面的狀況lua
del key
將B當前正在持有的鎖釋放了 (但B此時不知情,還覺得本身持有鎖,繼續執行本身的任務)(激活生命線表示有客戶端持有鎖,按顏色對應)spa
再來看看使用隨機值是怎麼避免這一問題的。code
首先,客戶端獲取鎖的時候,設置的value爲各個客戶端本身生成的隨機字符串。
在步驟5,因爲客戶端A和B的隨機值不同,只會走lua腳本里的else分支,而不進行刪除,這樣客戶端C天然也就沒有了可乘之機。
總的來講,隨機字符串是用做每一個客戶端的「標識」,來確保客戶端只能刪除本身獲取的鎖,而不會誤刪其餘客戶端的。
使用了隨機字符串後的時序圖
看到這,你也許會想:不對呀,客戶端B和C存在重疊的部分(圖1中紅藍),表示同時持有鎖了。但AB也有重疊部分啊(圖1中藍綠),這是否是有bug?
對不起,這還真不算bug,只能算是 feature。
由於要防止客戶端崩潰而致使鎖永不釋放,鎖的過時時間是不得不加的。
因此,這裏只能根據獲取鎖後要執行的任務內容,來設置一個合適的鎖過時時間。
或者採用部分語言中客戶端的方式,在過時前延長過時時間。
上面的例子中釋放鎖時用了這麼一個lua腳本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
複製代碼
爲何不直接在代碼裏寫呢?
let value = redis.get(key);
if(randStr == value){
redis.del(key);
}
複製代碼
很少廢話,直接上圖
圖中能夠看出,若是不使用lua腳本將「比較&刪除」這兩步合在一塊兒造成原子操做,那麼讀取和刪除這兩個步驟之間可能會出現原有鎖過時,又被另外一個客戶端B獲取的狀況。
而原客戶端A不知情,仍然執行了刪除,刪掉了客戶端B正常獲取的鎖,最終致使BC同時持有,不知足互斥性。
單個redis節點實現的分佈式鎖雖然通常來講夠用了,但記住它並不是徹底可靠。
前面的場景中都假設了Redis服務可以正常運行,但若是發生了主節點的切換,由從節點頂上,仍可能致使多客戶端都認爲本身持有鎖的狀況。
這個場景比較簡單:
因此若是須要避免單個redis節點崩潰切換後丟數據的問題,實現更高級別的保障,能夠用多個redis節點實現的 redlock
注意:即便是redlock,仍不能100%保證可靠,只是比單個redis更安全一些。