天天看點

redisson redlock

一、關于分布式鎖的兩篇文章

文章1

文章2

二、redis分布式鎖存在的問題

redis實作分布式鎖有很多種方案,

比較完善的方案應該是用setNx + lua進行實作。

簡單實作如下:

- java代碼-加鎖,相當于setnx lock_key_name unique_value
set lock_key_name unique_value NX PX 5000;

- lua腳本-解鎖,原子性操作
if redis.call("get", KEYS[1] == ARGV[1]) then
	return redis.call("del", KEYS[1])
else
    return 0
end
           

注意:

  1. value需要具有唯一性,

    可以采用時間戳、uuid或者自增id實作

  2. 用戶端在解鎖時,需要比較本地記憶體中的value和redis中的value是否一緻,防止誤解鎖;(case:clientA擷取鎖lock1,由于clientA執行的時間比較久,導緻key=lock1已經過期,redis執行個體會移除該key;clientB擷取相同的鎖lock1,clientB正在占有鎖并執行業務,此時clientA業務已經執行完畢,準備釋放鎖;如果沒有比較value的邏輯,那麼clientA會把clientB持有的鎖釋放掉,這個顯然不行的,由于value值不同,那麼clientA釋放鎖的時候隻會釋放自己加的鎖,不會誤釋放别的用戶端加的鎖)。

注意,

不能一味認為鎖過期的時間應該比key的expire要長

,因為接下來要介紹的redisson架構中有續期機制(看門狗機制),該機制的核心就是:

如果線程仍舊沒有執行完,那麼redisson會自動給redis中的目标key延長逾時時間

在分布式系統中,

為了避免單點故障,提高可靠性,redis都會采用主從架構,當主節點挂了後,從節點會作為主繼續提供服務。該種方案能夠滿足大多數的業務場景,但是對于要求強一緻性的場景如交易,該種方案還是有漏洞的

,原因如下:

  1. redis主從架構采用的是異步複制,當master節點拿到了鎖,但是鎖還未同步到slave節點,此時master節點挂了,發生故障轉移,slave節點被選舉為master節點,丢失了鎖。這樣其他線程就能夠擷取到該鎖,顯然是有問題的。
是以,

上述基于redis實作的分布式鎖隻是滿足了AP,并沒有滿足C

三、redlock

正是因為上述redis分布式鎖存在的一緻性問題,

redis作者提出了一個更加進階的基于redis實作的分布式鎖——RedLock

。原文可參考 Distributed locks with Redis,也可以參考這篇文章。

  1. RedLock是什麼

RedLock是基于redis實作的分布式鎖,它能夠保證以下特性:

  • 互斥性:在任何時候,隻能有一個用戶端能夠持有鎖;
  • 避免死鎖:當用戶端拿到鎖後,即使發生了網絡分區或者用戶端當機,也不會發生死鎖;(利用key的存活時間)
  • 容錯性:隻要多數節點的redis執行個體正常運作,就能夠對外提供服務,加鎖或者釋放鎖;

而非redLock是無法滿足互斥性的,上面已經闡述過了原因。

  1. RedLock算法

假設有N個redis的master節點,這些節點是互相獨立的(不需要主從或者其他協調的系統)。N推薦為奇數~

用戶端在擷取鎖時,需要做以下操作:

  1. 擷取目前時間戳,以微秒為機關。
  2. 使用相同的lockName和lockValue,嘗試從N個節點擷取鎖。(在擷取鎖時,要求等待擷取鎖的時間遠小于鎖的釋放時間,如鎖的lease_time為10s,那麼wait_time應該為5-50毫秒;避免因為redis執行個體挂掉,用戶端需要等待更長的時間才能傳回,即需要讓用戶端能夠fast_fail;如果一個redis執行個體不可用,那麼需要繼續從下個redis執行個體擷取鎖)
  3. 當從N個節點擷取鎖結束後,如果用戶端能夠從多數節點(N/2 +1)中成功擷取鎖,且擷取鎖的時間小于失效時間,那麼可認為,用戶端成功獲得了鎖。(擷取鎖的時間=目前時間戳 - 步驟1的時間戳)
  4. 用戶端成功獲得鎖後,那麼鎖的實際有效時間 = 設定鎖的有效時間 - 擷取鎖的時間。
  5. 用戶端擷取鎖失敗後,N個節點的redis執行個體都會釋放鎖,即使未能加鎖成功。
  • 注意:

    為什麼N推薦為奇數呢

  • 原因1:本着最大容錯的情況下,占用服務資源最少的原則,2N+1和2N+2的容災能力是一樣的,是以采用2N+1;比如,5台伺服器允許2台當機,容錯性為2,6台伺服器也隻能允許2台當機,容錯性也是2,因為要求超過半數節點存活才OK。
  • 原因2:假設有6個redis節點,client1和client2同時向redis執行個體擷取同一個鎖資源,那麼可能發生的結果是——client1獲得了3把鎖,client2獲得了3把鎖,由于都沒有超過半數,那麼client1和client2擷取鎖都失敗,對于奇數節點是不會存在這個問題。

參考文章

  1. 失敗時重試
  • 當用戶端無法擷取到鎖時,應該

    随機延時後進行重試,防止多個用戶端在同一時間搶奪同一資源的鎖(會導緻腦裂,最終都不能擷取到鎖)

    。用戶端

    獲得超過半數節點的鎖花費的時間越短,那麼腦裂的機率就越低。

    是以,理想的情況下,用戶端最好能夠同時(并發)向所有redis發出set指令。
  • 當用戶端

    從多數節點擷取鎖失敗時,應該盡快釋放已經成功擷取的鎖,這樣其他用戶端不需要等待鎖過期後再擷取。

    (如果存在網絡分區,用戶端已經無法和redis進行通信,那麼此時隻能等待鎖過期後自動釋放)

不明白為什麼會發生腦裂???

  1. 釋放鎖

向所有redis執行個體發送釋放鎖指令即可,不需要關心redis執行個體有沒有成功上鎖。

redisson在加鎖的時候,

key=lockName, value=uuid + threadID,采用set結構存儲,并包含了上鎖的次數(支援可重入);解鎖的時候通過hexists判斷key和value是否存在,存在則解鎖;這裡不會出現誤解鎖

  1. 性能、 崩潰恢複和redis同步
  • 如何提升分布式鎖的性能?以每分鐘執行多少次acquire/release操作作為性能名額,

    一方面通過增加redis執行個體可用降低響應延遲,另一方面,使用非阻塞模型,一次發送所有的指令,然後異步讀取響應結果,

    這裡假設用戶端和redis之間的RTT差不多。
  • 如果redis沒用使用備份,redis重新開機後,那麼會丢失鎖,導緻多個用戶端都能擷取到鎖。

    通過AOF持久化可以緩解這個問題。redis key過期是unix時間戳,即便是redis重新開機,那麼時間依然是前進的。

    但是,如果是斷電呢?redis在啟動後,可能就會丢失這個key(在寫入或者還未寫入磁盤時斷電了,取決于fsync的配置),如果采用fsync=always,那麼會極大影響性能。如何解決這個問題呢?可以讓redis節點重新開機後,在一個TTL時間段内,對用戶端不可用即可。

四、redisson對redlock的實作