既然是實現分佈式鎖,那確定得保證多個鏈接集中請求一個資源的排他性,而redis的單線程特性則很好的知足了這一需求。redis提供的set方法則是知足這一需求的關鍵,下圖是實現redis分佈式鎖的簡單流程,先有個初步的料及。redis
下面是set
命令的相關用法:分佈式
SET key value [EX seconds] [PX milliseconds] [NX|XX]
可選參數:lua
看似簡單的實現,實則有不少隱藏的坑,下面我將以幾個案例做爲分析.spa
熟悉redis命令的同窗可能注意到setnx
這個命令,而後再配合expire
命令是否能夠實現同樣的效果了?
SETEX user_id 10086 // 此處服務掛掉了,那麼user_id將永生 expire user_id
注意,這是2條命令也就意味着這是非原子性操做,當執行到第一條命令SETEX user_id 10086
,再準備給這個key設置失效時間時服務忽然掛掉了,那麼這個key將會永生,其餘請求將永遠沒法獲取到這把鎖。
因此,咱們須要用SET
保證其原子性. SET user_id 10086 EX 30 NX
,即設置key爲user_id,value爲10086,而且設置失效時間爲30s,若是該key存在則放棄更改。線程
在咱們設置key值的時候通常會盡可能保證他的惟一性,好比訂單ID,庫存ID等。而根據
SET
命令的返回結果來判斷是否有其餘請求強佔,貌似value值的設置無關緊要,事實真的如此嗎?
僞代碼:設計
SET user_id 10086 EX 30 NX // 處理業務中 //業務處理完畢 DEL user_id
流程圖分析:code
經過上圖咱們將請求的過程肢解,即分爲如下幾步:ip
DEL user_id
,可是他本身的鎖已經失效了,刪除的是請求B鎖。而請求B的業務此時並未處理完,因此此處就出現了問題!改進:
爲了不誤刪除別人的鎖,因此咱們須要在刪除鎖的時候須要判斷一下這個鎖是不是本身的。這個時候咱們設置的value就生效了,能夠經過value來判斷這把鎖是否屬於本身。 這個value值設置比較隨意,只要能作區分就能夠了。資源
僞代碼:get
SET user_id 10086 EX 30 NX // 處理業務中 //業務處理完畢 if( (GET user_id) == "XXX" ){ DEL user_id }
好吧,終於解決了這一系列坑本覺得就要完工 。正在得意回味本身改進的代碼時總以爲哪裏有點怪怪的,猛地發現這個 GET取值判斷和DEL刪除並不是原子操做。那麼接着上面的分析,會出現什麼問題呢?
if( (GET user_id) == "XXX" ){ //獲取到本身鎖後,進行取值判斷且判斷爲真。此時,這把鎖剛好失效。 DEL user_id }
當程序判經過該鎖的值判斷髮現這把鎖是本身加上的,準備DEL
。此時該鎖剛好失效,而另一個請求剛好得到key值爲user_id的鎖。
此時程序執行了了DEL user_id
,刪除了別人加的鎖,尷尬!
因此這段代碼並不完美,爲了保證查詢和刪除的原子性操做,須要引入lua腳本支持。
改進:
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
回到Case2中,請求A中的業務處理時長超過了鎖的失效時間。對於此類問題,看到不少網上大佬給出的答案是起一個守護線程進行監聽key的失效時間,而後在快要失效的時候爲期續命。
其實redis對於分佈式鎖,Redisson有着更好的實現方式。
代碼(稍後上傳....)