redis分布式鎖深度剖析

redis分布式鎖的實現(xiàn)主要是基于redis的setnx 命令(setnx命令解釋見:http://doc.redisfans.com/string/setnx.html),我們來看一下setnx命令的作用:

redis-setnx.png

1、redis分布式鎖的基本實現(xiàn)

redis加鎖命令:

SETNX resource_name my_random_value PX 30000

這個命令的作用是在只有這個key不存在的時候才會設(shè)置這個key的值(NX選項的作用),超時時間設(shè)為30000毫秒(PX選項的作用) 這個key的值設(shè)為“my_random_value”。這個值必須在所有獲取鎖請求的客戶端里保持唯一。

SETNX 值保持唯一的是為了確保安全的釋放鎖,避免誤刪其他客戶端得到的鎖。舉個例子,一個客戶端拿到了鎖,被某個操作阻塞了很長時間,過了超時時間后自動釋放了這個鎖,然后這個客戶端之后又嘗試刪除這個其實已經(jīng)被其他客戶端拿到的鎖。所以單純的用DEL指令有可能造成一個客戶端刪除了其他客戶端的鎖,通過校驗這個值保證每個客戶端都用一個隨機(jī)字符串’簽名’了,這樣每個鎖就只能被獲得鎖的客戶端刪除了。

既然釋放鎖時既需要校驗這個值又需要刪除鎖,那么就需要保證原子性,redis支持原子地執(zhí)行一個lua腳本,所以我們通過lua腳本實現(xiàn)原子操作。代碼如下:

if redis.call("get",KEYS[1]) == ARGV[1] then
         return redis.call("del",KEYS[1]) 
else 
         return 0 
end

2、業(yè)務(wù)邏輯執(zhí)行時間超出鎖的超時限制導(dǎo)致兩個客戶端同時持有鎖的問題

如果在加鎖和釋放鎖之間的邏輯執(zhí)行得太長,以至于超出了鎖的超時限制,就會出現(xiàn)問題。因為這時候第一個線程持有的鎖過期了,臨界區(qū)的邏輯還沒有執(zhí)行完,這個時候第二個線程就提前重新持有了這把鎖,導(dǎo)致臨界區(qū)代碼不能得到嚴(yán)格的串行執(zhí)行。

不難發(fā)現(xiàn)正常情況下鎖操作完后都會被手動釋放,常見的解決方案是調(diào)大鎖的超時時間,之后若再出現(xiàn)超時帶來的并發(fā)問題,人工介入修正數(shù)據(jù)。這也不是一個完美的方案,因為但業(yè)務(wù)邏輯執(zhí)行時間是不可控的,所以還是可能出現(xiàn)超時,當(dāng)前線程的邏輯沒有執(zhí)行完,其它線程乘虛而入。并且如果鎖超時時間設(shè)置過長,當(dāng)持有鎖的客戶端宕機(jī),釋放鎖就得依靠redis的超時時間,這將導(dǎo)致業(yè)務(wù)在一個超時時間周期內(nèi)不可用。

基本上,如果在執(zhí)行計算期間發(fā)現(xiàn)鎖快要超時了,客戶端可以給redis服務(wù)實例發(fā)送一個Lua腳本讓redis服務(wù)端延長鎖的時間,只要這個鎖的key還存在而且值還等于客戶端設(shè)置的那個值。 客戶端應(yīng)當(dāng)只有在失效時間內(nèi)無法延長鎖時再去重新獲取鎖(基本上這個和獲取鎖的算法是差不多的)。

當(dāng)鎖超時時間快到期且邏輯未執(zhí)行完,延長鎖超時時間的偽代碼:

if  redis.call("get",KEYS[1]) == ARGV[1] then 
        redis.call("set",KEYS[1],ex=3000)
else 
        getDLock();//重新獲取鎖

3、redis的單點故障主從切換帶來的兩個客戶端同時持有鎖的問題

生產(chǎn)中redis一般是主從模式,主節(jié)點掛掉時,從節(jié)點會取而代之,客戶端上卻并沒有明顯感知。原先第一個客戶端在主節(jié)點中申請成功了一把鎖,但是這把鎖還沒有來得及同步到從節(jié)點,主節(jié)點突然掛掉了。然后從節(jié)點變成了主節(jié)點,這個新的節(jié)點內(nèi)部沒有這個鎖,所以當(dāng)另一個客戶端過來請求加鎖時,立即就批準(zhǔn)了。這樣就會導(dǎo)致系統(tǒng)中同樣一把鎖被兩個客戶端同時持有,不安全性由此產(chǎn)生。

不過這種不安全也僅僅是在主從發(fā)生 failover 的情況下才會產(chǎn)生,而且持續(xù)時間極短,業(yè)務(wù)系統(tǒng)多數(shù)情況下可以容忍。

4、RedLock算法

如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,可以考慮 redlock。 Redlock 算法是由Antirez 發(fā)明的,它的流程比較復(fù)雜,不過已經(jīng)有了很多開源的 library 做了良好的封裝,用戶可以拿來即用,比如 redlock-py。

import redlock

addrs = [{
  "host": "localhost",
  "port": 6379,
  "db": 0
}, {
  "host": "localhost",
  "port": 6479,
  "db": 0
}, {
  "host": "localhost",
  "port": 6579,
  "db": 0
}]
dlm = redlock.Redlock(addrs)
success = dlm.lock("user-lck-laoqian", 5000)
if success:
    print 'lock success'
    dlm.unlock('user-lck-laoqian')
else:
    print 'lock failed'

RedLock算法的核心原理:

使用N個完全獨立、沒有主從關(guān)系的Redis master節(jié)點以保證他們大多數(shù)情況下都不會同時宕機(jī),N一般為奇數(shù)。一個客戶端需要做如下操作來獲取鎖:

1.獲取當(dāng)前時間(單位是毫秒)。
2.輪流用相同的key和隨機(jī)值在N個節(jié)點上請求鎖,在這一步里,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節(jié)點鎖請求的超時時間可能是5-50毫秒的范圍,這個可以防止一個客戶端在某個宕掉的master節(jié)點上阻塞過長時間,如果一個master節(jié)點不可用了,我們應(yīng)該盡快嘗試下一個master節(jié)點。
3.客戶端計算第二步中獲取鎖所花的時間,只有當(dāng)客戶端在大多數(shù)master節(jié)點上成功獲取了鎖((N/2) +1),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認(rèn)為是獲取成功了。
4.如果鎖獲取成功了,那現(xiàn)在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。
5.如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節(jié)點上釋放鎖,即便是那些他認(rèn)為沒有獲取成功的鎖。

5、知識擴(kuò)展

5.1為什么lua腳本結(jié)合redis命令可以實現(xiàn)原子性

Redis 提供了非常豐富的指令集,但是用戶依然不滿足,希望可以自定義擴(kuò)充若干指令來完成一些特定領(lǐng)域的問題。Redis 為這樣的用戶場景提供了 lua 腳本支持,用戶可以向服務(wù)器發(fā)送 lua 腳本來執(zhí)行自定義動作,獲取腳本的響應(yīng)數(shù)據(jù)。Redis 服務(wù)器會單線程原子性執(zhí)行 lua 腳本,保證 lua 腳本在處理的過程中不會被任意其它請求打斷。


redis-lua交互--.png

5.2 redis 可重入分布式鎖

要實現(xiàn)可重入鎖,方法很簡單,當(dāng)加鎖失敗時判斷鎖的值是不是跟當(dāng)前線程設(shè)置值相同,偽代碼如下:

if setnx == 0 
      if get(key) == my_random_value 
            //重入 
      else 
           //不可重入 
else 
      //獲取了鎖,等價于可重入

參考文檔:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容