溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么使用redis實(shí)現(xiàn)分布式鎖

發(fā)布時間:2021-06-12 17:02:14 來源:億速云 閱讀:212 作者:小新 欄目:大數(shù)據(jù)

小編給大家分享一下怎么使用redis實(shí)現(xiàn)分布式鎖,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

1.加鎖

最簡單的方法是使用setnx命令。key是鎖的唯一標(biāo)識,按業(yè)務(wù)來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名為 “l(fā)ock_hot_商品ID” 。而value設(shè)置成什么呢?我們可以姑且設(shè)置成1。加鎖的偽代碼如下:

setnx(key,1)

當(dāng)一個線程執(zhí)行setnx返回1,說明key原本不存在,該線程成功得到了鎖;當(dāng)一個線程執(zhí)行setnx返回0,說明key已經(jīng)存在,該線程搶鎖失敗。

2.解鎖

有加鎖就得有解鎖。當(dāng)?shù)玫芥i的線程執(zhí)行完任務(wù),需要釋放鎖,以便其他線程可以進(jìn)入。釋放鎖的最簡單方式是執(zhí)行del指令,偽代碼如下:

del(key)

釋放鎖之后,其他線程就可以繼續(xù)執(zhí)行setnx命令來獲得鎖。

3.鎖超時

鎖超時是什么意思呢?如果一個得到鎖的線程在執(zhí)行任務(wù)的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠(yuǎn)被鎖住,別的線程再也別想進(jìn)來。
所以,setnx的key必須設(shè)置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間后自動釋放。setnx不支持超時參數(shù),所以需要額外的指令,

偽代碼如下:

expire(key, 30)

4.綜合分析

我們分布式鎖實(shí)現(xiàn)的第一版?zhèn)未a如下:

if(setnx(key,1) == 1){
    expire(key,30)
    try {
        do something ......
    } finally {
        del(key)
    }
}
  • 上面的偽代碼中,存在著三個致命問題:

1. setnx和expire的非原子性

設(shè)想一個極端場景,當(dāng)某線程執(zhí)行setnx,成功得到了鎖:

setnx剛執(zhí)行成功,還未來得及執(zhí)行expire指令,節(jié)點(diǎn)1 Duang的一聲掛掉了。

這樣一來,這把鎖就沒有設(shè)置過期時間,變得“長生不老”,別的線程再也無法獲得鎖了。
怎么解決呢?setnx指令本身是不支持傳入超時時間的,但是Redis 2.6.12以上版本為set指令增加了可選參數(shù),偽代碼如下:

set(key,1,30,NX)
  • 這樣就可以取代setnx指令。

2. del 導(dǎo)致誤刪

又是一個極端場景,假如某線程成功得到了鎖,并且設(shè)置的超時時間是30秒。

如果某些原因?qū)е戮€程A執(zhí)行的很慢很慢,過了30秒都沒執(zhí)行完,這時候鎖過期自動釋放,線程B得到了鎖。

隨后,線程A執(zhí)行完了任務(wù),線程A接著執(zhí)行del指令來釋放鎖。但這時候線程B還沒執(zhí)行完,線程A實(shí)際上刪除的是線程B加的鎖。

怎么避免這種情況呢?可以在del釋放鎖之前做一個判斷,驗(yàn)證當(dāng)前的鎖是不是自己加的鎖。
至于具體的實(shí)現(xiàn),可以在加鎖的時候把當(dāng)前的線程ID當(dāng)做value,并在刪除之前驗(yàn)證key對應(yīng)的value是不是自己線程的ID。
加鎖:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
  •  

解鎖:

if(threadId .equals(redisClient.get(key))){

    del(key)

}
  •  

但是,這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨(dú)立操作,不是原子性。
我們都是追求極致的程序員,所以這一塊要用Lua腳本來實(shí)現(xiàn):

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
  •  

這樣一來,驗(yàn)證和刪除過程就是原子操作了。

3. 出現(xiàn)并發(fā)的可能性

還是剛才第二點(diǎn)所描述的場景,雖然我們避免了線程A誤刪掉key的情況,但是同一時間有A,B兩個線程在訪問代碼塊,仍然是不完美的。
怎么辦呢?我們可以讓獲得鎖的線程開啟一個守護(hù)線程,用來給快要過期的鎖“續(xù)航”。

當(dāng)過去了29秒,線程A還沒執(zhí)行完,這時候守護(hù)線程會執(zhí)行expire指令,為這把鎖“續(xù)命”20秒。守護(hù)線程從第29秒開始執(zhí)行,每20秒執(zhí)行一次。

當(dāng)線程A執(zhí)行完任務(wù),會顯式關(guān)掉守護(hù)線程。

另一種情況,如果節(jié)點(diǎn)1 忽然斷電,由于線程A和守護(hù)線程在同一個進(jìn)程,守護(hù)線程也會停下。這把鎖到了超時的時候,沒人給它續(xù)命,也就自動釋放了。

以上是“怎么使用redis實(shí)現(xiàn)分布式鎖”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI