Redis有一系列的命令,特點是以NX結(jié)尾,NX是Not eXists的縮寫,如SETNX命令就應(yīng)該理解為:SET if Not eXists。這系列的命令非常有用,這里講使用SETNX來實現(xiàn)分布式鎖。
用SETNX實現(xiàn)分布式鎖
利用SETNX非常簡單地實現(xiàn)分布式鎖。例如:某客戶端要獲得一個名字foo的鎖,客戶端使用下面的命令進行獲?。?/p>
SETNX lock.foo <current Unix time + lock timeout + 1>
- 如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設(shè)置為時間值表示該鍵已被鎖定,該客戶端最后可以通過DEL lock.foo來釋放該鎖。
- 如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。
解決死鎖
上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎么解決?我們可以通過鎖的鍵對應(yīng)的時間戳來判斷這種情況是否發(fā)生了,如果當(dāng)前的時間已經(jīng)大于lock.foo的值,說明該鎖已失效,可以被重新使用。
發(fā)生這種情況時,可不能簡單的通過DEL來刪除鎖,然后再SETNX一次,當(dāng)多個客戶端檢測到鎖超時后都會嘗試去釋放它,這里就可能出現(xiàn)一個競態(tài)條件,讓我們模擬一下這個場景:
- C0操作超時了,但它還持有著鎖,C1和C2讀取lock.foo檢查時間戳,先后發(fā)現(xiàn)超時了。
- C1 發(fā)送DEL lock.foo
- C1 發(fā)送SETNX lock.foo 并且成功了。
- C2 發(fā)送DEL lock.foo
- C2 發(fā)送SETNX lock.foo 并且成功了。
這樣一來,C1,C2都拿到了鎖!問題大了!
幸好這種問題是可以避免D,讓我們來看看C3這個客戶端是怎樣做的:
- C3發(fā)送SETNX lock.foo 想要獲得鎖,由于C0還持有鎖,所以Redis返回給C3一個0
- C3發(fā)送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試。
- 反之,如果已超時,C3通過下面的操作來嘗試獲得鎖:
GETSET lock.foo <current Unix time + lock timeout + 1> - 通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如愿以償拿到鎖了。
- 如果在C3之前,有個叫C4的客戶端比C3快一步執(zhí)行了上面的操作,那么C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒拿到鎖,但它改寫了C4設(shè)置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。
注意:為了讓分布式鎖的算法更穩(wěn)鍵些,持有鎖的客戶端在解鎖之前應(yīng)該再檢查一次自己的鎖是否已經(jīng)超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,操作完的時候鎖因為超時已經(jīng)被別人獲得,這時就不必解鎖了。
示例偽代碼
根據(jù)上面的代碼,我寫了一小段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?行,怎樣舒服怎樣用吧,別重復(fù)代碼就行。
這樣只能作同一主機的不同進程/線程的鎖吧?
Redis 自個兒不是分布式的哪,這怎么分布式鎖呢?
我所定義的分布式鎖是指支持分布式應(yīng)用(多個進程,多個主機)共享的鎖。而不是說鎖本身是分布式的。
鎖本身若是分布式,那它的可靠性也就值得商榷了。。
我使用Redis這個機制實現(xiàn)鎖,就是為了可以讓程序可以實現(xiàn)分布式部署啊。
我理解的分布式鎖中的鎖是指存在應(yīng)用進程外的鎖,傳統(tǒng)的鎖是虛擬機(或進程)級別的鎖,是活在某個進程內(nèi)的,使用虛擬機級別的鎖的程序是沒辦法或很難大規(guī)?;蛯崿F(xiàn)分布式部署的。
如有理解不當(dāng),懇請大媽指正啊。
jeff的思路是對的,分布式鎖是指使用鎖的程序是分布的,一般來說是多個不同的進程,而不是說鎖的實現(xiàn)容器redis本身的實現(xiàn)是不是分布(集群)
為什么不用expire來實現(xiàn)超時?
expire只能讓Key失效,如果有新的進程在Key過期后拿到了新的鎖,原來超時的進程回來如果不經(jīng)判斷會誤認為那是他持有的鎖,會將鎖誤刪了。
隨機數(shù)和watch可以解決這個問題
————-
random_key = getrandomkey()
value = redis.setnx(key, random_key)
if value == 1:
redis.expire(key, timeout)
do_job()
redis.watch(mykey)
value = redis.get(mykey)
if random_key == value:
redis.multi()
redis.del(mykey)
redis.exec
else:
#不是自己的鎖,所以不能刪除
else:
#沒拿到鎖