使用Redis指令setnx、expire、getset等操作實(shí)現(xiàn)互斥資源的訪問(wèn)
本文內(nèi)容來(lái)著網(wǎng)絡(luò)整理,參考:
http://www.linuxidc.com/Linux/2014-12/110958.htm
http://www.jeffkit.info/2011/07/1000/
http://blog.csdn.net/java2000_wl/article/details/8740911
2. 背景在特殊業(yè)務(wù)邏輯中,需要保證莫一個(gè)操作同時(shí)只有一個(gè)線程在操作,保證數(shù)據(jù)一致性。防止數(shù)據(jù)被多次改寫或產(chǎn)生多條重復(fù)數(shù)據(jù)。
3. 思路通過(guò)get 和set 命令實(shí)現(xiàn)這種方式很容易想到,就是當(dāng)每次請(qǐng)求到來(lái)時(shí)通過(guò)get判斷這個(gè)鎖是否存在,如果不存在則set創(chuàng)建。這種方法有一個(gè)弊端,由于get和set是兩次Redis請(qǐng)求,二者之間有延時(shí),在高并發(fā)的環(huán)境下,有可能在get檢測(cè)到鎖不存之后在set之前已經(jīng)被其他線程set,這時(shí)當(dāng)前線程再set,這樣鎖就失效了。所以這種方法只能應(yīng)對(duì)并發(fā)量不是很高的情況
通過(guò)setnx 和 expire命令實(shí)現(xiàn)在訪問(wèn)需要互斥訪問(wèn)的資源時(shí),通過(guò)setnx命令去設(shè)置一個(gè)lock 鍵,setnx的作用是判斷鎖是否存在,如果不存在則創(chuàng)建,返回成功,如果存在則返回失敗,服務(wù)器返回給客戶端,指示客戶端稍后重試。expire命令用于給該鎖設(shè)定一個(gè)過(guò)期時(shí)間,用于防止線程crash,導(dǎo)致鎖一直有效,從而導(dǎo)致死鎖。例如:設(shè)定鎖的有效期為100秒,那么即使線程奔潰,在100秒后鎖會(huì)自動(dòng)失效。(實(shí)際上,這個(gè)地方也有問(wèn)題,高并發(fā)下在執(zhí)行expire命令時(shí)偶爾會(huì)失敗(Redis socket鏈接問(wèn)題),失敗后這個(gè)lock就不會(huì)自動(dòng)過(guò)期,值會(huì)會(huì)一直存在,出現(xiàn)死鎖導(dǎo)致后續(xù)的重試操作就永遠(yuǎn)不會(huì)成功! 為保證執(zhí)行成功需要考慮失敗時(shí)多次執(zhí)行expire)
setnx lock "lock"
expire lock 100 //如果鎖定成功,則設(shè)置過(guò)期時(shí)間
do work code //工作邏輯代碼
del lock //訪問(wèn)互斥資源結(jié)束后,刪除鎖
通過(guò)setnx和 getset命令加 timespan+timeout (推薦)如何解決setnx + expire 的死鎖問(wèn)題?可以通過(guò)鎖的鍵對(duì)應(yīng)的時(shí)間戳來(lái)判斷這種情況是否發(fā)生了,如果當(dāng)前的時(shí)間已經(jīng)大于lock的值,說(shuō)明該鎖已失效,可以被重新使用。
發(fā)生這種情況時(shí),可不能簡(jiǎn)單的通過(guò)DEL來(lái)刪除鎖,然后再SETNX一次,當(dāng)多個(gè)客戶端檢測(cè)到鎖超時(shí)后都會(huì)嘗試去釋放它,這里就可能出現(xiàn)一個(gè)競(jìng)態(tài)條件,讓我們模擬一下這個(gè)場(chǎng)景:
C0操作超時(shí)了,但它還持有著鎖,C1和C2讀取lock檢查時(shí)間戳,先后發(fā)現(xiàn)超時(shí)了。
C1 發(fā)送DEL lock
C1 發(fā)送SETNX lock并且成功了。
C2 發(fā)送DEL lock
C2 發(fā)送SETNX lock并且成功了。
這樣一來(lái),C1,C2都拿到了鎖!問(wèn)題大了!
幸好這種問(wèn)題是可以避免的,讓我們來(lái)看看C3這個(gè)客戶端是怎樣做的:
C3發(fā)送SETNX lock想要獲得鎖,由于C0還持有鎖,所以Redis返回給C3一個(gè)0
C3發(fā)送GET lock以檢查鎖是否超時(shí)了,如果沒(méi)超時(shí),則等待或重試。
反之,如果已超時(shí),C3通過(guò)下面的操作來(lái)嘗試獲得鎖:
GETSET lock <current Unix time + lock timeout + 1>
通過(guò)GETSET,C3拿到的時(shí)間戳如果仍然是超時(shí)的,那就說(shuō)明,C3如愿以償拿到鎖了。
如果在C3之前,有個(gè)叫C4的客戶端比C3快一步執(zhí)行了上面的操作,那么C3拿到的時(shí)間戳是個(gè)未超時(shí)的值,這時(shí),C3沒(méi)有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒(méi)拿到鎖,但它改寫了C4設(shè)置的鎖的超時(shí)值,不過(guò)這一點(diǎn)非常微小的誤差帶來(lái)的影響可以忽略不計(jì)。
注意:為了讓分布式鎖的算法更穩(wěn)鍵些,持有鎖的客戶端在解鎖之前應(yīng)該再檢查一次自己的鎖是否已經(jīng)超時(shí),再去做DEL操作,因?yàn)榭赡芸蛻舳艘驗(yàn)槟硞€(gè)耗時(shí)的操作而掛起,操作完的時(shí)候鎖因?yàn)槌瑫r(shí)已經(jīng)被別人獲得,這時(shí)就不必解鎖了
附偽代碼:
# 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
4. 代碼通過(guò)setnx 和 expire命令實(shí)現(xiàn)代碼 1 public boolean tryLock(String key, int timeout, int expiretime, int sleeptime) throws Exception { 2 3 4 5 Jedis redis = jedisPool.getResource(); 6 7 try { 8 9 long nano = System.nanoTime();10 11 do {12 13 Long i = redis.setnx(key, "key");14 15 jedisPool.returnResource(redis);16 17 if (i == 1) {18 19 redis.expire(key, expiretime);20 21 return Boolean.TRUE;22 23 }24 25 if (timeout == 0) {26 27 break;28 29 }30 31 Thread.sleep(sleeptime);32 33 } while ((System.nanoTime() - nano) < TimeUnit.SECONDS.toNanos(timeout));34 35 return Boolean.FALSE;36 37 } catch (RuntimeException | InterruptedException e) {38 39 if (redis != null) {40 41 jedisPool.returnBrokenResource(redis);42 43 }44 45 throw e;46 47 }48 49 }50 51 通過(guò)setnx和 getset命令加 timespan+timeout (推薦代碼) 1 public boolean tryLock(String key, int timeout, int expiretime, int sleeptime) throws Exception { 2 3 4 5 Jedis redis = jedisPool.getResource(); 6 7 try { 8 9 long nano = System.nanoTime();10 11 12 13 do {14 15 long timestamp = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiretime) + 1;16 17 Long i = redis.setnx(key, String.valueOf(timestamp));18 19 jedisPool.returnResource(redis);20 21 if (i == 1) {22 23 return Boolean.TRUE;24 25 }26 27 String lockVal = getString(key);28 29 if (StringUtils.isBlank(lockVal) || !StringUtils.isNumeric(lockVal)) {30 31 lockVal = "0";32 33 }34 35 if (System.currentTimeMillis() > Long.valueOf(lockVal)) {36 37 lockVal = getAndset(key, String.valueOf(timestamp));38 39 if (StringUtils.isBlank(lockVal) || !StringUtils.isNumeric(lockVal)) {40 41 lockVal = "0";42 43 }44 45 if (System.currentTimeMillis() > Long.valueOf(lockVal)) {46 47 return Boolean.TRUE;48 49 }50 51 }52 53 if (timeout == 0) {54 55 break;56 57 }58 59 Thread.sleep(sleeptime);60 61 } while ((System.nanoTime() - nano) < TimeUnit.SECONDS.toNanos(timeout));62 63 return Boolean.FALSE;64 65 } catch (RuntimeException | InterruptedException e) {66 67 if (redis != null) {68 69 jedisPool.returnBrokenResource(redis);70 71 }72 73 throw e;74 75 }76 77 }新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注