圖文詳解Redis分佈式鎖(單節點)

本文重點並不在於提供一個可運行的Redis分佈式鎖示例,而是結合圖文理解redis的分佈式鎖實現上的細節,以及爲何要這樣作。redis

實現方式

用僞代碼的形式簡單介紹實現方式安全

獲取鎖

SET resource_name my_random_value NX EX 30
複製代碼
  • my_random_value: 一個隨機值,同一key的競爭者這個值都不能同樣
  • NX : 只在鍵不存在時, 纔對鍵進行設置操做
  • EX : 設置過時秒數

釋放鎖

經過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

  1. 客戶端A獲取鎖,執行任務 (Redis客戶端,實際是一個服務器節點)
  2. A執行中被阻塞,卡住了
  3. 鎖超時自動釋放了,但A仍在繼續執行
  4. 客戶端B獲取鎖,開始執行它本身的任務
  5. A緩過來了,任務完成執行,而後一句del key 將B當前正在持有的鎖釋放了 (但B此時不知情,還覺得本身持有鎖,繼續執行本身的任務)
  6. 另有客戶端C獲取鎖,這時客戶端B和C同時持有,不知足互斥性了

(激活生命線表示有客戶端持有鎖,按顏色對應)spa

再來看看使用隨機值是怎麼避免這一問題的。code

首先,客戶端獲取鎖的時候,設置的value爲各個客戶端本身生成的隨機字符串。

在步驟5,因爲客戶端A和B的隨機值不同,只會走lua腳本里的else分支,而不進行刪除,這樣客戶端C天然也就沒有了可乘之機。

總的來講,隨機字符串是用做每一個客戶端的「標識」,來確保客戶端只能刪除本身獲取的鎖,而不會誤刪其餘客戶端的。

使用了隨機字符串後的時序圖

看到這,你也許會想:不對呀,客戶端B和C存在重疊的部分(圖1中紅藍),表示同時持有鎖了。但AB也有重疊部分啊(圖1中藍綠),這是否是有bug?

對不起,這還真不算bug,只能算是 feature

由於要防止客戶端崩潰而致使鎖永不釋放,鎖的過時時間是不得不加的。

因此,這裏只能根據獲取鎖後要執行的任務內容,來設置一個合適的鎖過時時間。

或者採用部分語言中客戶端的方式,在過時前延長過時時間。


爲何釋放鎖的「比較&刪除」須要用到lua腳本實現原子操做?

上面的例子中釋放鎖時用了這麼一個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服務可以正常運行,但若是發生了主節點的切換,由從節點頂上,仍可能致使多客戶端都認爲本身持有鎖的狀況。

這個場景比較簡單:

  1. 客戶端A獲取鎖
  2. redis主節點宕機,但設鎖對應的鍵還沒來得及同步到從節點上
  3. 從節點被選爲新的主節點,丟失了鍵
  4. 另有客戶端B獲取鎖,AB同時認爲本身持有鎖

因此若是須要避免單個redis節點崩潰切換後丟數據的問題,實現更高級別的保障,能夠用多個redis節點實現的 redlock

注意:即便是redlock,仍不能100%保證可靠,只是比單個redis更安全一些。


參考

redis.io/topics/dist…

相關文章
相關標籤/搜索