Redis有一系列的指令,特點是以NX結尾,NX是Not eXists的縮寫,如SETNX指令就應該了解為:SET if Not eXists。這系列的指令非常有用,這裡講使用SETNX來實作分布式鎖。
用SETNX實作分布式鎖
利用SETNX非常簡單地實作分布式鎖。例如:某用戶端要獲得一個名字foo的鎖,用戶端使用下面的指令進行擷取:
SETNX lock.foo
如傳回1,則該用戶端獲得鎖,把lock.foo的鍵值設定為時間值表示該鍵已被鎖定,該用戶端最後可以通過DEL lock.foo來釋放該鎖。
如傳回0,表明該鎖已被其他用戶端取得,這時我們可以先傳回或進行重試等對方完成或等待鎖逾時。
解決死鎖
上面的鎖定邏輯有一個問題:如果一個持有鎖的用戶端失敗或崩潰了不能釋放鎖,該怎麼解決?我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發生了,如果目前的時間已經大于lock.foo的值,說明該鎖已失效,可以被重新使用。
發生這種情況時,可不能簡單的通過DEL來删除鎖,然後再SETNX一次,當多個用戶端檢測到鎖逾時後都會嘗試去釋放它,這裡就可能出現一個競态條件,讓我們模拟一下這個場景:
C0操作逾時了,但它還持有着鎖,C1和C2讀取lock.foo檢查時間戳,先後發現逾時了。
C1 發送DEL lock.foo
C1 發送SETNX lock.foo 并且成功了。
C2 發送DEL lock.foo
C2 發送SETNX lock.foo 并且成功了。
這樣一來,C1,C2都拿到了鎖!問題大了!
幸好這種問題是可以避免D,讓我們來看看C3這個用戶端是怎樣做的:
C3發送SETNX lock.foo 想要獲得鎖,由于C0還持有鎖,是以Redis傳回給C3一個0
C3發送GET lock.foo 以檢查鎖是否逾時了,如果沒逾時,則等待或重試。
反之,如果已逾時,C3通過下面的操作來嘗試獲得鎖:
GETSET lock.foo
通過GETSET,C3拿到的時間戳如果仍然是逾時的,那就說明,C3如願以償拿到鎖了。
如果在C3之前,有個叫C4的用戶端比C3快一步執行了上面的操作,那麼C3拿到的時間戳是個未逾時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒拿到鎖,但它改寫了C4設定的鎖的逾時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。
注意:為了讓分布式鎖的算法更穩鍵些,持有鎖的用戶端在解鎖之前應該再檢查一次自己的鎖是否已經逾時,再去做DEL操作,因為可能用戶端因為某個耗時的操作而挂起,操作完的時候鎖因為逾時已經被别人獲得,這時就不必解鎖了。
示例僞代碼
根據上面的代碼,我寫了一小段Fake代碼來描述使用分布式鎖的全過程:
# get lock
lock = 0
while lock != 1:
timestamp = current Unix time + lock timeout + 1
lock = SETNX lock.foo timestamp
if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
break;
else:
sleep(10ms)
# do your job
do_job()
# release
if now() < GET lock.foo:
DEL lock.foo
是的,要想這段邏輯可以重用,使用python的你馬上就想到了Decorator,而用Java的你是不是也想到了那誰?AOP + annotation?行,怎樣舒服怎樣用吧,别重複代碼就行。
熬夜不易,點選請老王喝杯烈酒!!!!!!!