SETNX命令簡(jiǎn)介
SETNX key value
將 key 的值設(shè)為 value捉捅,當(dāng)且僅當(dāng) key 不存在。
若給定的 key 已經(jīng)存在寄月,則 SETNX 不做任何動(dòng)作无牵。
返回整數(shù)漾肮,具體為
- 1克懊,當(dāng) key 的值被設(shè)置
- 0谭溉,當(dāng) key 的值沒(méi)被設(shè)置
多個(gè)進(jìn)程執(zhí)行以下Redis命令:
SETNX lock.lock <current time + timeout +1 > ? //其中1是cover鎖獲取時(shí)間
如果 SETNX 返回1扮念,說(shuō)明該進(jìn)程獲得鎖柜与,SETNX將鍵 lock.lock 的值設(shè)置為鎖的超時(shí)時(shí)間(當(dāng)前時(shí)間 + 鎖的有效時(shí)間)弄匕。
如果 SETNX 返回0沽瞭,說(shuō)明其他進(jìn)程已經(jīng)獲得了鎖驹溃,進(jìn)程不能進(jìn)入臨界區(qū)。進(jìn)程可以在一個(gè)循環(huán)中不斷地嘗試 SETNX 操作,以獲得鎖傍药。
1. 如果進(jìn)程獲得鎖后拐辽,斷開(kāi)了與 Redis 的連接(可能是進(jìn)程掛掉拣挪,或者網(wǎng)絡(luò)中斷),如果沒(méi)有有效的釋放鎖的機(jī)制俱诸,那么其他進(jìn)程都會(huì)處于一直等待的狀態(tài)菠劝,即出現(xiàn)“死鎖”。
2. 在使用 SETNX 獲得鎖時(shí)睁搭,我們將鍵 lock.lock 的值設(shè)置為鎖的有效時(shí)間赶诊,進(jìn)程獲得鎖后,其他進(jìn)程還會(huì)不斷的檢測(cè)鎖是否已超時(shí)园骆,如果超時(shí)舔痪,那么等待的進(jìn)程也將有機(jī)會(huì)獲得鎖。當(dāng)前進(jìn)程需要在超時(shí)時(shí)退出锌唾,否則超時(shí)后锄码,其他進(jìn)程有可能拿到鎖導(dǎo)致多個(gè)進(jìn)程同時(shí)拿到鎖。
3. 鎖超時(shí)時(shí)晌涕,我們不能簡(jiǎn)單地使用 DEL 命令刪除鍵 lock.lock 以釋放鎖重窟。考慮以下情況,進(jìn)程P1已經(jīng)首先獲得了鎖 lock.lock知给,然后進(jìn)程P1掛掉了。進(jìn)程P2,P3正在不斷地檢測(cè)鎖是否已釋放或者已超時(shí)花墩,執(zhí)行流程如下:
P2和P3進(jìn)程讀取鍵 lock.lock 的值,檢測(cè)鎖是否已超時(shí)(通過(guò)比較當(dāng)前時(shí)間和鍵 lock.lock 的值來(lái)判斷是否超時(shí))武氓,P2和P3進(jìn)程發(fā)現(xiàn)鎖 lock.lock 已超時(shí)弱睦,P2執(zhí)行 DEL lock.lock命令况木,P2執(zhí)行 SETNX lock.lock命令奔垦,并返回1,即P2獲得鎖。P3執(zhí)行 DEL lock.lock命令將P2剛剛設(shè)置的鍵 lock.lock 刪除(這步是由于P3剛才已檢測(cè)到鎖已超時(shí))鸯旁。P3執(zhí)行 SETNX lock.lock命令残炮,并返回1楷怒,即P3獲得鎖。P2和P3同時(shí)獲得了鎖。
從上面的情況可以得知,在檢測(cè)到鎖超時(shí)后,進(jìn)程不能直接簡(jiǎn)單地執(zhí)行 DEL 刪除鍵的操作以獲得鎖芹啥。
為了解決上述算法可能出現(xiàn)的多個(gè)進(jìn)程同時(shí)獲得鎖的問(wèn)題傀履,我們?cè)賮?lái)看以下的算法絮宁。
我們同樣假設(shè)進(jìn)程P1已經(jīng)首先獲得了鎖 lock.lock,然后進(jìn)程P1掛掉了。接下來(lái)的情況:
進(jìn)程P4執(zhí)行 SETNX lock.lock 以嘗試獲取鎖
由于進(jìn)程P1已獲得了鎖泪蔫,所以P4執(zhí)行 SETNX lock.lock 返回0,即獲取鎖失敗
P4執(zhí)行 GET lock.lock 來(lái)檢測(cè)鎖是否已超時(shí),如果沒(méi)超時(shí)朽合,則等待一段時(shí)間休讳,再次檢測(cè)
如果P4檢測(cè)到鎖已超時(shí),即當(dāng)前的時(shí)間大于鍵 lock.lock 的值尚骄,P4會(huì)執(zhí)行以下操作
GETSET lock.lock
由于 GETSET 操作在設(shè)置鍵的值的同時(shí)需五,還會(huì)返回鍵的舊值泽示,通過(guò)比較鍵 lock.lock 的舊值是否小于當(dāng)前時(shí)間械筛,可以判斷進(jìn)程是否已獲得鎖
假如另一個(gè)進(jìn)程P5也檢測(cè)到鎖已超時(shí),并在P4之前執(zhí)行了 GETSET 操作闯狱,那么P4的 GETSET 操作返回的是一個(gè)大于當(dāng)前時(shí)間的時(shí)間戳,這樣P4就不會(huì)獲得鎖而繼續(xù)等待吹截。
//實(shí)例代碼
LOCK_TIMEOUT =3
lock =0
lock_timeout =0
lock_key ='lock.lock'# 獲取鎖
while lock !=1:
now =int(time.time())
lock_timeout = now + LOCK_TIMEOUT +1
lock = redis_client.setnx(lock_key, lock_timeout)
if lock ==1 or (now >int(redis_client.get(lock_key))) and now > int(redis_client.getset(lock_key, lock_timeout)): ? //解決當(dāng)鎖超時(shí)時(shí)媒抠,兩個(gè)進(jìn)程同時(shí)判斷鎖可以獲取阀趴,只能其中一個(gè)可以設(shè)置成功
break
else:time.sleep(0.001)
# 已獲得鎖
do_job()
# 釋放鎖
now =int(time.time())
if now < lock_timeout:
redis_client.delete(lock_key)
有很多已經(jīng)實(shí)現(xiàn)好的基于redis的分布式鎖的封裝检碗,詳細(xì)見(jiàn) https://redis.io/topics/distlock