分布式鎖是在分布式環境下(多個jvm程序)控制多個客戶端對某一資源的同步訪問的一種實現,與之相對應的是執行緒鎖,執行緒鎖控制的是同乙個jvm程序內多個執行緒之間的同步。分布式鎖的一般實現方法是在應用伺服器之外通過乙個共享的儲存伺服器儲存鎖資源,同一時刻只有乙個客戶端能占有鎖資源來完成。通常有基於zookeeper,redis,或資料庫三種實現形式。本文介紹基於redis的實現方案。
基於redis實現分布式鎖需要滿足如下幾點要求:
在分布式集群中,被分布式鎖控制的方法或**段同一時刻只能被乙個客戶端上面的乙個執行緒執行,也就是互斥
鎖資訊需要設定過期時間,避免乙個執行緒長期占有(比如在做解鎖操作前異常退出)而導致死鎖
加鎖與解鎖必須一致,誰加的鎖,就由誰來解(或過期超時),乙個客戶端不能解開另乙個客戶端加的鎖
加鎖與解鎖的過程必須保證原子性
1. 加鎖實現
基於redis的分布式鎖加鎖操作一般使用 setnx 命令,其含義是「將 key 的值設為 value ,當且僅當 key 不存在。若給定的 key 已經存在,則 setnx 不做任何動作」。
在 spring boot 中,可以使用 stringredistemplate 來實現,如下,一行**即可實現加鎖過程。(下列**給出兩種呼叫形式——立即返回加鎖結果與給定超時時間獲取加鎖結果)
/**
* 嘗試獲取鎖(立即返回)
* @param key 鎖的redis key
* @param value 鎖的value
* @return 是否獲取成功
*/public boolean lock
(string key, string value,
long expire)
/** * 嘗試獲取鎖,並至多等待timeout時長
** @param key 鎖的redis key
* @param value 鎖的value
* @param timeout 超時時長
* @param unit 時間單位
* @return 是否獲取成功
*/public boolean lock
(string key, string value,
long expire,
long timeout, timeunit unit)
catch (interruptedexception e)
", key, e);}
waitalready +
= waitmillisper;}if
(waitalready < waitmillis)
log.
warn
("<*****= lock {} failed after waiting for {} ms"
, key, waitalready)
;return false;
}
上述實現如何滿足前面提到的幾點要求:
客戶端互斥: 可以將expire過期時間設定為大於同步**的執行時間,比如同步**塊執行時間為1s,則可將expire設定為3s或5s。避免同步**執行過程中expire時間到,其它客戶端又可以獲取鎖執行同步**塊。
通過設定過期時間expire來避免某個客戶端長期占有鎖。
通過value來控制誰加的鎖,由誰解的邏輯,比如可以使用requestid作為value,requestid唯一標記一次請求。
setifabsent方法 底層通過呼叫 redis 的 setnx 命令,操作具備原子性。
錯誤示例:
網上有如下實現,
public boolean lock
(string key, string value,
long expire)
return result;
}
該實現的問題是如果在result為true,但還沒成功設定expire時,程式異常退出了,將導致該鎖一直被占用而導致死鎖,不滿足第二點要求。
2. 解鎖實現
解鎖也需要滿足前面所述的四個要求,實現**如下:
private static final string release_lock_lua_script =
"if redis.call('get', keys[1]) == ar**[1] then return redis.call('del', keys[1]) else return 0 end"
;private static final long release_lock_success_result =1l;
/** * 釋放鎖
* @param key 鎖的redis key
* @param value 鎖的value
*/public boolean unlock
(string key, string value)
這段實現使用乙個lua指令碼來實現解鎖操作,保證操作的原子性。傳入的value值需與該執行緒加鎖時的value一致,可以使用requestid(具體實現下面給出)。
錯誤示例:
public boolean unlock
(string key, string value)
}
該實現先獲取鎖的當前值,判斷兩值相等則刪除。考慮一種極端情況,如果在判斷為true時,剛好該鎖過期時間到,另乙個客戶端加鎖成功,則接下來的delete將不管三七二十一將別人加的鎖直接刪掉了,不滿足第三點要求。該示例主要是因為沒***解鎖操作的原子性導致。
3. 註解支援
為了方便使用,新增乙個註解,可以放於方法上控制方法在分布式環境中的同步執行。
/**
* 標註在方法上的分布式鎖註解
*/@retention
(retentionpolicy.runtime)
@target
(elementtype.method)
public @inte***ce distributedlockable
新增乙個切面來解析註解的處理,
/**
* 分布式鎖註解處理切面
*/@aspect
@slf4j
public class distributedlockaspect
/** * 在方法上執行同步鎖
*/@around
(value =
"@annotation(lockable)"
) public object distlock
(proceedingjoinpoint point, distributedlockable lockable) throws throwable
else
", key)
;return null;
}} catch (exception e)
finally failed, maybe locked by another client already. "
, lockable.
key())
;}}}
}}
requestid 的實現如下,通過註冊乙個filter,在請求開始時生成乙個uuid存於threadlocal中,在請求返回時清除。
public class webutil
public static string getrequestid()
return requestid;
} public static
void
removerequestid()
}public class requestidfilter implements filter
webutil.
setrequestid
(reqid)
; try finally }}
//在配置類中註冊filter
/*** 新增requestid
* @return
*/@bean
public filterregistrationbean requestidfilter()
4. 使用註解
@distributedlockable
(key =
"test"
, expire =10)
public void
test()
catch (interruptedexception e)
system.out.
println
("執行緒-"
+thread.
currentthread()
.getname()
+"結束執行..."
+ localdatetime.
now())
;}
本文給出了基於redis的分布式鎖的實現方案與常見的錯誤示例。要保障分布式鎖的正確執行,需滿足本文所提的四個要求,尤其注意保證加鎖解鎖操作的原子性,設定過期時間,及對同乙個鎖的加鎖解鎖執行緒一致。 基於redis的分布式鎖
public class redislock 加鎖 取到鎖加鎖,並返回值用於解鎖 取不到鎖則立即返回 1 param millitimeout 最長鎖定時間,超時後自動刪除鎖,避免死鎖 return public synchronized long lock long millitimeout lo...
基於twemproxy的redis分布式應用
根據以往的測試結論,單個redis的例項的記憶體總量最好控制在8g以內 最大不能超過20g 而實際上應用對redis的記憶體的需求可能會遠遠大於8g,因此需要乙個保持redis server效能不下降,但可以有效擴充redis server的容量的方案。twemproxy是乙個恰當的選擇。b 簡介 ...
基於 Redis 的分布式鎖
分布式鎖在分布式應用中應用廣泛,想要搞懂乙個新事物首先得了解它的由來,這樣才能更加的理解甚至可以舉一反三。首先談到分布式鎖自然也就聯想到分布式應用。在我們將應用拆分為分布式應用之前的單機系統中,對一些併發場景讀取公共資源時如扣庫存,賣車票之類的需求可以簡單的使用同步或者是加鎖就可以實現。但是應用分布...