解決Redis分佈式鎖業務代碼超時致使鎖失效問題

一、redis分佈式鎖的基本實現

redis加鎖命令:redis

SETNX resource_name my_random_value PX 30000

這個命令的做用是在只有這個key不存在的時候纔會設置這個key的值(NX選項的做用),超時時間設爲30000毫秒(PX選項的做用) 這個key的值設爲「my_random_value」。這個值必須在全部獲取鎖請求的客戶端裏保持惟一。算法

SETNX 值保持惟一的是爲了確保安全的釋放鎖,避免誤刪其餘客戶端獲得的鎖。舉個例子,一個客戶端拿到了鎖,被某個操做阻塞了很長時間,過了超時時間後自動釋放了這個鎖,而後這個客戶端以後又嘗試刪除這個其實已經被其餘客戶端拿到的鎖。因此單純的用DEL指令有可能形成一個客戶端刪除了其餘客戶端的鎖,經過校驗這個值保證每一個客戶端都用一個隨機字符串’簽名’了,這樣每一個鎖就只能被得到鎖的客戶端刪除了。安全

既然釋放鎖時既須要校驗這個值又須要刪除鎖,那麼就須要保證原子性,redis支持原子地執行一個lua腳本,因此咱們經過lua腳本實現原子操做。代碼以下:服務器

if redis.call("get",KEYS[1]) == ARGV[1] then

         return redis.call("del",KEYS[1])     

else

         return 0

end

二、業務邏輯執行時間超出鎖的超時限制致使兩個客戶端同時持有鎖的問題

若是在加鎖和釋放鎖之間的邏輯執行得太長,以致於超出了鎖的超時限制,就會出現問題。由於這時候第一個線程持有的鎖過時了,臨界區的邏輯尚未執行完,這個時候第二個線程就提早從新持有了這把鎖,致使臨界區代碼不能獲得嚴格的串行執行。併發

不難發現正常狀況下鎖操做完後都會被手動釋放,常見的解決方案是調大鎖的超時時間,以後若再出現超時帶來的併發問題,人工介入修正數據。這也不是一個完美的方案,由於但業務邏輯執行時間是不可控的,因此仍是可能出現超時,當前線程的邏輯沒有執行完,其它線程乘虛而入。而且若是鎖超時時間設置過長,當持有鎖的客戶端宕機,釋放鎖就得依靠redis的超時時間,這將致使業務在一個超時時間週期內不可用。dom

基本上,若是在執行計算期間發現鎖快要超時了,客戶端能夠給redis服務實例發送一個Lua腳本讓redis服務端延長鎖的時間,只要這個鎖的key還存在並且值還等於客戶端設置的那個值。 客戶端應當只有在失效時間內沒法延長鎖時再去從新獲取鎖(基本上這個和獲取鎖的算法是差很少的)。分佈式

啓動另一個線程去檢查的問題,這個key是否超時,在某個時間還沒釋放。lua

當鎖超時時間快到期且邏輯未執行完,延長鎖超時時間的僞代碼:spa

if redis.call("get",KEYS[1]) == ARGV[1] then

          redis.call("set",KEYS[1],ex=3000)  

else

          getDLock();//從新獲取鎖

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

生產中redis通常是主從模式,主節點掛掉時,從節點會取而代之,客戶端上卻並無明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,可是這把鎖尚未來得及同步到從節點,主節點忽然掛掉了。而後從節點變成了主節點,這個新的節點內部沒有這個鎖,因此當另外一個客戶端過來請求加鎖時,當即就批准了。這樣就會致使系統中一樣一把鎖被兩個客戶端同時持有,不安全性由此產生。線程

不過這種不安全也僅僅是在主從發生 failover 的狀況下才會產生,並且持續時間極短,業務系統多數狀況下能夠容忍。

四、RedLock算法

若是你很在意高可用性,但願掛了一臺 redis 徹底不受影響,能夠考慮 redlock。 Redlock 算法是由Antirez 發明的,它的流程比較複雜,不過已經有了不少開源的 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個徹底獨立、沒有主從關係的Redis master節點以保證他們大多數狀況下都不會同時宕機,N通常爲奇數。一個客戶端須要作以下操做來獲取鎖:

1.獲取當前時間(單位是毫秒)。

2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裏,客戶端在每一個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。好比若是鎖自動釋放時間是10秒鐘,那每一個節點鎖請求的超時時間多是5-50毫秒的範圍,這個能夠防止一個客戶端在某個宕掉的master節點上阻塞過長時間,若是一個master節點不可用了,咱們應該儘快嘗試下一個master節點。

3.客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖((N/2) +1),並且總共消耗的時間不超過鎖釋放時間,這個鎖就認爲是獲取成功了。

4.若是鎖獲取成功了,那如今鎖自動釋放時間就是最初的鎖釋放時間減去以前獲取鎖所消耗的時間。

5.若是鎖獲取失敗了,不論是由於獲取成功的鎖不超過一半(N/2+1)仍是由於總消耗時間超過了鎖釋放時間,客戶端都會到每一個master節點上釋放鎖,即使是那些他認爲沒有獲取成功的鎖。

五、知識擴展

5.1爲何lua腳本結合redis命令能夠實現原子性

Redis 提供了很是豐富的指令集,可是用戶依然不知足,但願能夠自定義擴充若干指令來完成一些特定領域的問題。Redis 爲這樣的用戶場景提供了 lua 腳本支持,用戶能夠向服務器發送 lua 腳原本執行自定義動做,獲取腳本的響應數據。Redis 服務器會單線程原子性執行 lua 腳本,保證 lua 腳本在處理的過程當中不會被任意其它請求打斷。

img

5.2 redis 可重入分佈式鎖

要實現可重入鎖,方法很簡單,當加鎖失敗時判斷鎖的值是否是跟當前線程設置值相同,僞代碼以下:

if setnx == 0

    if get(key) == my_random_value

        //重入

    else

        //不可重入

else

     //獲取了鎖,等價於可重入
本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索