1. 程式人生 > >redis分散式鎖深度剖析

redis分散式鎖深度剖析

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

redis-setnx.png

 

1、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

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

如果在加鎖和釋放鎖之間的邏輯執行得太長,以至於超出了鎖的超時限制,就會出現問題。因為這時候第一個執行緒持有的鎖過期了,臨界區的邏輯還沒有執行完,這個時候第二個執行緒就提前重新持有了這把鎖,導致臨界區程式碼不能得到嚴格的序列執行。

不難發現正常情況下鎖操作完後都會被手動釋放,常見的解決方案是調大鎖的超時時間,之後若再出現超時帶來的併發問題,人工介入修正資料。這也不是一個完美的方案,因為但業務邏輯執行時間是不可控的,所以還是可能出現超時,當前執行緒的邏輯沒有執行完,其它執行緒乘虛而入。並且如果鎖超時時間設定過長,當持有鎖的客戶端宕機,釋放鎖就得依靠redis的超時時間,這將導致業務在一個超時時間週期內不可用。

基本上,如果在執行計算期間發現鎖快要超時了,客戶端可以給redis服務例項傳送一個Lua指令碼讓redis服務端延長鎖的時間,只要這個鎖的key還存在而且值還等於客戶端設定的那個值。 客戶端應當只有在失效時間內無法延長鎖時再去重新獲取鎖(基本上這個和獲取鎖的演算法是差不多的)。

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

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

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

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

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

4、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、知識擴充套件

5.1為什麼lua指令碼結合redis命令可以實現原子性

Redis 提供了非常豐富的指令集,但是使用者依然不滿足,希望可以自定義擴充若干指令來完成一些特定領域的問題。Redis 為這樣的使用者場景提供了 lua 指令碼支援,使用者可以向伺服器傳送 lua 指令碼來執行自定義動作,獲取指令碼的響應資料。Redis 伺服器會單執行緒原子性執行 lua 指令碼,保證 lua 指令碼在處理的過程中不會被任意其它請求打斷。

 

redis-lua互動--.png

5.2 redis 可重入分散式鎖

要實現可重入鎖,方法很簡單,當加鎖失敗時判斷鎖的值是不是跟當前執行緒設定值相同,虛擬碼如下:

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

參考文件: