分佈式鎖的實現及問題

在多線程併發的狀況下,咱們可使用鎖來保證一個代碼塊在同一時間內只能由一個線程訪問。好比Java的synchronized關鍵字和Reentrantlock類等等。redis

這樣子能夠保證在同一個JVM進程內的多個線程同步執行。多線程

 

若是在分佈式的集羣環境中,如何保證不一樣節點的線程同步執行呢?併發

 

怎麼才能在分佈式系統中,實現不一樣線程對代碼和資源的同步訪問?分佈式

對於單進程的併發場景,咱們可使用語言和類庫提供的鎖。對於分佈式場景,咱們可使用分佈式鎖。.net

那麼怎麼才能實現分佈式系統中的鎖呢?線程

分佈式鎖有許多中實現方法,下面簡單列舉一下。設計

分佈式鎖的實現有哪些?3d

1.Memcached分佈式鎖blog

利用Memcached的add命令。此命令是原子性操做,只有在key不存在的狀況下,才能add成功,也就意味着線程獲得了鎖。隊列

2.Redis分佈式鎖

和Memcached的方式相似,利用Redis的setnx命令。此命令一樣是原子性操做,只有在key不存在的狀況下,才能set成功。(setnx命令並不完善,後續會介紹替代方案)

3.Zookeeper分佈式鎖

利用Zookeeper的順序臨時節點,來實現分佈式鎖和等待隊列。Zookeeper設計的初衷,就是爲了實現分佈式鎖服務的。

首先講一下Redis的分佈式鎖,這種實現方式比較有表明性。

如何用Redis實現分佈式鎖?

Redis分佈式鎖的基本流程並不難理解,但要想寫得盡善盡美,也並非那麼容易。在這裏,咱們須要先了解分佈式鎖實現的三個核心要素:

1.加鎖

最簡單的方法是使用setnx命令。key是鎖的惟一標識,按業務來決定命名。好比想要給一種商品的秒殺活動加鎖,能夠給key命名爲 「lock_sale_商品ID」 。而value設置成什麼呢?鎖的value值爲一個隨機生成的UUID。咱們能夠姑且設置成1。加鎖的僞代碼以下:    

setnx(key,1)
當一個線程執行setnx返回1,說明key本來不存在,該線程成功獲得了鎖;當一個線程執行setnx返回0,說明key已經存在,該線程搶鎖失敗。

2.解鎖

有加鎖就得有解鎖。當獲得鎖的線程執行完任務,須要釋放鎖,以便其餘線程能夠進入。釋放鎖的最簡單方式是執行del指令,僞代碼以下:

del(key)
釋放鎖以後,其餘線程就能夠繼續執行setnx命令來得到鎖。

3.鎖超時

鎖超時是什麼意思呢?若是一個獲得鎖的線程在執行任務的過程當中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的線程再也別想進來。

因此,setnx的key必須設置一個超時時間,單位爲second,以保證即便沒有被顯式釋放,這把鎖也要在必定時間後自動釋放,避免死鎖。setnx不支持超時參數,因此須要額外的指令,僞代碼以下:

expire(key, 30)
綜合起來,咱們分佈式鎖實現的初版僞代碼以下:

if(setnx(key,1) == 1){
    expire(key,30)
    try {
        do something ......
    } finally {
        del(key)
    }
}
上面的僞代碼只是分佈式鎖的簡單實現,結合實際應用場景考慮就會發現上述分佈式鎖的實現存在着三個致命問題:

1. setnx和expire的非原子性

設想一個極端場景,當某線程執行setnx,成功獲得了鎖:

 

setnx剛執行成功,還將來得及執行expire指令,節點1 Duang的一聲掛掉了。 

 

這樣一來,這把鎖就沒有設置過時時間,變得「長生不老」,別的線程再也沒法得到鎖了。

怎麼解決呢?setnx指令自己是不支持傳入超時時間的,幸虧Redis 2.6.12以上版本爲set指令增長了可選參數,僞代碼以下:

set(key,1,30,NX)
這樣就能夠取代setnx指令。

2. del 致使誤刪

又是一個極端場景,假如某線程成功獲得了鎖,而且設置的超時時間是30秒。

 

若是某些緣由致使線程A執行的很慢很慢,過了30秒都沒執行完,這時候鎖過時自動釋放,線程B獲得了鎖。

 

隨後,線程A執行完了任務,線程A接着執行del指令來釋放鎖。但這時候線程B還沒執行完,線程A實際上刪除的是線程B加的鎖。 

 

怎麼避免這種狀況呢?能夠在del釋放鎖以前作一個判斷,驗證當前的鎖是否是本身加的鎖。

至於具體的實現,能夠在加鎖的時候把當前的線程ID當作value,並在刪除以前驗證key對應的value是否是本身線程的ID。

加鎖:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
解鎖:

if(threadId .equals(redisClient.get(key))){
    del(key)
}
也能夠在釋放鎖的時候,經過鎖的默認value值UUID判斷是否是該鎖,如果該鎖,則執行delete進行鎖釋放。

可是,這樣作又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操做,不是原子性的。

要想實現驗證和刪除過程的原子性,可使用Lua腳原本實現。這樣就能保證驗證和刪除過程的正確性了。

3. 出現併發的可能性

仍是剛纔第二點所描述的場景,雖然咱們避免了線程A誤刪掉key的狀況,可是同一時間有A,B兩個線程在訪問代碼塊,仍然是不完美的。

怎麼辦呢?咱們可讓得到鎖的線程開啓一個守護線程,用來給快要過時的鎖「續航」。

 

當過去了29秒,線程A還沒執行完,這時候守護線程會執行expire指令,爲這把鎖「續命」20秒。守護線程從第29秒開始執行,每20秒執行一次。

 

當線程A執行完任務,會顯式關掉守護線程。

 

另外一種狀況,若是節點1 突然斷電,因爲線程A和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。

 

關於Redis分佈式鎖的內容就介紹到這裏啦。 ————————————————版權聲明:本文爲CSDN博主「kongmin_123」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。原文連接:https://blog.csdn.net/kongmin_123/article/details/82080962

相關文章
相關標籤/搜索