一、關于分布式鎖的兩篇文章
文章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
注意:
- value需要具有唯一性,
;
可以采用時間戳、uuid或者自增id實作
- 用戶端在解鎖時,需要比較本地記憶體中的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都會采用主從架構,當主節點挂了後,從節點會作為主繼續提供服務。該種方案能夠滿足大多數的業務場景,但是對于要求強一緻性的場景如交易,該種方案還是有漏洞的
,原因如下:
是以,
- redis主從架構采用的是異步複制,當master節點拿到了鎖,但是鎖還未同步到slave節點,此時master節點挂了,發生故障轉移,slave節點被選舉為master節點,丢失了鎖。這樣其他線程就能夠擷取到該鎖,顯然是有問題的。
。
上述基于redis實作的分布式鎖隻是滿足了AP,并沒有滿足C
三、redlock
正是因為上述redis分布式鎖存在的一緻性問題,
redis作者提出了一個更加進階的基于redis實作的分布式鎖——RedLock
。原文可參考 Distributed locks with Redis,也可以參考這篇文章。
- RedLock是什麼
RedLock是基于redis實作的分布式鎖,它能夠保證以下特性:
- 互斥性:在任何時候,隻能有一個用戶端能夠持有鎖;
- 避免死鎖:當用戶端拿到鎖後,即使發生了網絡分區或者用戶端當機,也不會發生死鎖;(利用key的存活時間)
- 容錯性:隻要多數節點的redis執行個體正常運作,就能夠對外提供服務,加鎖或者釋放鎖;
而非redLock是無法滿足互斥性的,上面已經闡述過了原因。
- RedLock算法
假設有N個redis的master節點,這些節點是互相獨立的(不需要主從或者其他協調的系統)。N推薦為奇數~
用戶端在擷取鎖時,需要做以下操作:
- 擷取目前時間戳,以微秒為機關。
- 使用相同的lockName和lockValue,嘗試從N個節點擷取鎖。(在擷取鎖時,要求等待擷取鎖的時間遠小于鎖的釋放時間,如鎖的lease_time為10s,那麼wait_time應該為5-50毫秒;避免因為redis執行個體挂掉,用戶端需要等待更長的時間才能傳回,即需要讓用戶端能夠fast_fail;如果一個redis執行個體不可用,那麼需要繼續從下個redis執行個體擷取鎖)
- 當從N個節點擷取鎖結束後,如果用戶端能夠從多數節點(N/2 +1)中成功擷取鎖,且擷取鎖的時間小于失效時間,那麼可認為,用戶端成功獲得了鎖。(擷取鎖的時間=目前時間戳 - 步驟1的時間戳)
- 用戶端成功獲得鎖後,那麼鎖的實際有效時間 = 設定鎖的有效時間 - 擷取鎖的時間。
- 用戶端擷取鎖失敗後,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擷取鎖都失敗,對于奇數節點是不會存在這個問題。
參考文章
- 失敗時重試
- 當用戶端無法擷取到鎖時,應該
。用戶端
随機延時後進行重試,防止多個用戶端在同一時間搶奪同一資源的鎖(會導緻腦裂,最終都不能擷取到鎖)
是以,理想的情況下,用戶端最好能夠同時(并發)向所有redis發出set指令。
獲得超過半數節點的鎖花費的時間越短,那麼腦裂的機率就越低。
- 當用戶端
(如果存在網絡分區,用戶端已經無法和redis進行通信,那麼此時隻能等待鎖過期後自動釋放)
從多數節點擷取鎖失敗時,應該盡快釋放已經成功擷取的鎖,這樣其他用戶端不需要等待鎖過期後再擷取。
不明白為什麼會發生腦裂???
- 釋放鎖
向所有redis執行個體發送釋放鎖指令即可,不需要關心redis執行個體有沒有成功上鎖。
redisson在加鎖的時候, key=lockName, value=uuid + threadID,采用set結構存儲,并包含了上鎖的次數(支援可重入);解鎖的時候通過hexists判斷key和value是否存在,存在則解鎖;這裡不會出現誤解鎖
- 性能、 崩潰恢複和redis同步
- 如何提升分布式鎖的性能?以每分鐘執行多少次acquire/release操作作為性能名額,
這裡假設用戶端和redis之間的RTT差不多。
一方面通過增加redis執行個體可用降低響應延遲,另一方面,使用非阻塞模型,一次發送所有的指令,然後異步讀取響應結果,
- 如果redis沒用使用備份,redis重新開機後,那麼會丢失鎖,導緻多個用戶端都能擷取到鎖。
但是,如果是斷電呢?redis在啟動後,可能就會丢失這個key(在寫入或者還未寫入磁盤時斷電了,取決于fsync的配置),如果采用fsync=always,那麼會極大影響性能。如何解決這個問題呢?可以讓redis節點重新開機後,在一個TTL時間段内,對用戶端不可用即可。
通過AOF持久化可以緩解這個問題。redis key過期是unix時間戳,即便是redis重新開機,那麼時間依然是前進的。