Redis有一系列的命令头镊,特點(diǎn)是以NX結(jié)尾磷雇,NX是Not eXists的縮寫,如SETNX命令就應(yīng)該理解為:SET if Not eXists辣卒。這系列的命令非常有用掷贾,這里講使用SETNX來實(shí)現(xiàn)分布式鎖。
用SETNX實(shí)現(xiàn)分布式鎖
利用SETNX非常簡單地實(shí)現(xiàn)分布式鎖荣茫。例如:某客戶端要獲得一個(gè)名字foo的鎖想帅,客戶端使用下面的命令進(jìn)行獲取:
SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1啡莉,則該客戶端獲得鎖港准,把lock.foo的鍵值設(shè)置為時(shí)間值表示該鍵已被鎖定,該客戶端最后可以通過DEL lock.foo來釋放該鎖咧欣。
如返回0浅缸,表明該鎖已被其他客戶端取得,這時(shí)我們可以先返回或進(jìn)行重試等對(duì)方完成或等待鎖超時(shí)魄咕。
解決死鎖
上面的鎖定邏輯有一個(gè)問題:如果一個(gè)持有鎖的客戶端失敗或崩潰了不能釋放鎖衩椒,該怎么解決?我們可以通過鎖的鍵對(duì)應(yīng)的時(shí)間戳來判斷這種情況是否發(fā)生了哮兰,如果當(dāng)前的時(shí)間已經(jīng)大于lock.foo的值毛萌,說明該鎖已失效,可以被重新使用喝滞。
發(fā)生這種情況時(shí)阁将,可不能簡單的通過DEL來刪除鎖,然后再SETNX一次右遭,當(dāng)多個(gè)客戶端檢測到鎖超時(shí)后都會(huì)嘗試去釋放它做盅,這里就可能出現(xiàn)一個(gè)競態(tài)條件,讓我們模擬一下這個(gè)場景:
C0操作超時(shí)了,但它還持有著鎖窘哈,C1和C2讀取lock.foo檢查時(shí)間戳吹榴,先后發(fā)現(xiàn)超時(shí)了。
C1 發(fā)送DEL lock.foo
C1 發(fā)送SETNX lock.foo 并且成功了滚婉。
C2 發(fā)送DEL lock.foo
C2 發(fā)送SETNX lock.foo 并且成功了腊尚。
這樣一來,C1满哪,C2都拿到了鎖婿斥!問題大了!
幸好這種問題是可以避免D哨鸭,讓我們來看看C3這個(gè)客戶端是怎樣做的:
C3發(fā)送SETNX lock.foo 想要獲得鎖民宿,由于C0還持有鎖,所以Redis返回給C3一個(gè)0
C3發(fā)送GET lock.foo 以檢查鎖是否超時(shí)了像鸡,如果沒超時(shí)活鹰,則等待或重試哈恰。
反之,如果已超時(shí)志群,C3通過下面的操作來嘗試獲得鎖:
GETSET lock.foo <current Unix time + lock timeout + 1>
通過GETSET着绷,C3拿到的時(shí)間戳如果仍然是超時(shí)的,那就說明锌云,C3如愿以償拿到鎖了荠医。
如果在C3之前,有個(gè)叫C4的客戶端比C3快一步執(zhí)行了上面的操作桑涎,那么C3拿到的時(shí)間戳是個(gè)未超時(shí)的值彬向,這時(shí),C3沒有如期獲得鎖攻冷,需要再次等待或重試娃胆。留意一下,盡管C3沒拿到鎖等曼,但它改寫了C4設(shè)置的鎖的超時(shí)值里烦,不過這一點(diǎn)非常微小的誤差帶來的影響可以忽略不計(jì)。
注意:為了讓分布式鎖的算法更穩(wěn)鍵些禁谦,持有鎖的客戶端在解鎖之前應(yīng)該再檢查一次自己的鎖是否已經(jīng)超時(shí)胁黑,再去做DEL操作,因?yàn)榭赡芸蛻舳艘驗(yàn)槟硞€(gè)耗時(shí)的操作而掛起枷畏,操作完的時(shí)候鎖因?yàn)槌瑫r(shí)已經(jīng)被別人獲得别厘,這時(shí)就不必解鎖了虱饿。