Redis實現分佈式鎖進化
1、SetNX
tryLock(){
SETNX $key $value
}
release(){
DELETE $key
}
如果業務執行因爲某些原因意外退出了,導致創建了鎖但是沒有刪除鎖,那麼這個鎖將一直存在
2、SetNX + SetEx
tryLock(){
SETNX $key $value
EXPIRE $key Seconds
}
release(){
DELETE $key
}
由於SETNX、EXPIRE這是兩條Redis命令,不具有原子性,如果程序在執行完SETNX()之後突然崩潰,導致鎖沒有設置過期時間。那麼將會發生死鎖。
3、SET NX EX
redis 在2.6.12版本過後,有了一個新的決絕方案
SET key value [EX seconds] [PX millisecounds] [NX|XX]
EX seconds:設置鍵的過期時間爲second秒
PX millisecounds:設置鍵的過期時間爲millisecounds 毫秒
NX:只在鍵不存在的時候,纔對鍵進行設置操作
XX:只在鍵已經存在的時候,纔對鍵進行設置操作
SET操作成功後,返回的是OK,失敗返回NIL
tryLock(){
SET $key $value NX EX Seconds
}
release(){
DELETE $key
}
使用SET代替SETNX ,相當於SETNX+EXPIRE實現了原子性,不必擔心SETNX成功,EXPIRE失敗的問題!
有效的避免死鎖
如果一個請求業務處理的時間比較長,甚至比鎖的有效期還要長,導致在業務處理過程中,鎖就失效了,此時另一個請求會獲取鎖,但前一個請求在業務處理完畢的時候,如果不加以判斷直接刪除鎖,就會出現誤刪除其它請求創建的鎖的情況
4、SET NX EX uuid
tryLock(){
SET $key uuid NX EX Seconds
}
release(){
if ($redis->get($key) == $uuid) {
DELETE Key
}
}
release方法中,get和del並非原子操作,如果if判斷的時候,redis中的值等於uuid,但是在刪除之前又被其他線程設置爲其他值了,則會刪除其他線程的鎖。
5、SET NX EX uuid + Lua
tryLock(){
SET $key $uuid NX EX Seconds
}
release(){
jedis.eval("if redis.call('get', $lockKey) == $uuid then return redis.call('del', $lockKey) else return 0 end")
}
eval()方法可以確保原子性,eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,並且直到eval命令執行完成,Redis纔會執行其他命令。
這種實現方式有3大要點:
- set命令要用
set key value ex Seconds nx
; - value要具有唯一性;
- 釋放鎖時要驗證value值,不能誤解鎖;
事實上這類瑣最大的缺點就是它加鎖時只作用在一個Redis節點上,及時redis做了master-slave複製,但是在複製前發生了主從切換,master會丟掉一部分數據
-
在Redis的master節點上拿到了鎖;
-
但是這個加鎖的key還沒有同步到slave節點;
-
master故障,發生故障轉移,slave節點升級爲master節點;
-
導致鎖丟失。
6、RedLock
Redis作者antirez基於分佈式環境下提出了一種更高級的分佈式鎖的實現方式:Redlock。
antirez提出的redlock算法大概是這樣的:
在Redis的分佈式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。
爲了取到鎖,客戶端應該執行以下操作:
-
獲取當前Unix時間,以毫秒爲單位。
-
依次嘗試從5個實例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個Redis實例請求獲取鎖。
-
客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,這裏是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。
-
如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
-
如果因爲某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
RedLock中,爲了防止死鎖,鎖是具有過期時間的。
- 如果 Thread 1 在持有鎖的時候,發生了一次很長時間的 FGC 超過了鎖的過期時間。鎖就被釋放了。
- 這個時候 Thread 2 又獲得了一把鎖,提交數據。
- 這個時候 Thread 1 從 FGC 中甦醒過來了,又一次提交數據。
這還了得,數據就發生了錯誤。RedLock 只是保證了鎖的高可用性,並沒有保證鎖的正確性。
RedLock缺點:
- 對於提升效率的場景下,RedLock 太重。
- 對於對正確性要求極高的場景下,RedLock 並不能保證正確性。
建議:
如果使用redis實現分佈式鎖,建議不要使用RedLock,太重會影響性能
如果對性能要求極高,建議不要使用redis實現分佈式鎖,而應該採用Zookeeper實現
7、redisson
redisson框架已經有對分佈式可重入鎖、包括RedLock算法鎖的完成實現,我們在項目中可以使用redisson來實現分佈式鎖
redisson實現分佈式鎖示例代碼:
RLock lock = redissonClient.getLock("service2:lock:" + id);
boolean isLock = false;
try {
//嘗試加鎖,最多等待50ms,上鎖以後30s自動解鎖
isLock = lock.tryLock(2000, 30000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("lock error", e);
}
if (isLock) {
try {
//do something...
} finally {
if(lock.isHeldByCurrentThread()){
//手動釋放鎖
lock.unlock();
}
}
}