漫畫:什麼是分佈式鎖?

原文鏈接:http://www.360doc.com/content/18/0528/08/36490684_757590223.shtml

 

 

 

 

 

—————  第二天  —————

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

————————————

 

 

 

 

 

 

 

 

 

 

 

 

分佈式鎖的實現有哪些?

 

1.Memcached分佈式鎖

 

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

 

2.Redis分佈式鎖

 

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

 

3.Zookeeper分佈式鎖

 

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

 

4.Chubby

 

Google公司實現的粗粒度分佈式鎖服務,底層利用了Paxos一致性算法。

 

 

 

 

 

 

 

如何用Redis實現分佈式鎖?

 

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

 

1.加鎖

 

最簡單的方法是使用setnx命令。key是鎖的唯一標識,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名爲 “lock_sale_商品ID” 。而value設置成什麼呢?我們可以姑且設置成1。加鎖的僞代碼如下:    

 

setnx(key,1)

 

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

 

 

2.解鎖

 

有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行del指令,僞代碼如下:

 

del(key)

 

釋放鎖之後,其他線程就可以繼續執行setnx命令來獲得鎖。

 

 

3.鎖超時

 

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

 

所以,setnx的key必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。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秒。

 

 

 

 

如果某些原因導致線程B執行的很慢很慢,過了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)

}

 

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

 

我們都是追求極致的程序員,所以這一塊要用Lua腳本來實現:

 

 

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));

 

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

 

 

 

3. 出現併發的可能性

 

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

 

怎麼辦呢?我們可以讓獲得鎖的線程開啓一個守護線程,用來給快要過期的鎖“續航”。

 

 

 

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

 

 

 

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

 

 

 

 

另一種情況,如果節點1 忽然斷電,由於線程A和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。

 

 

 

 

守護線程的代碼並不難實現,有了大體思路,大家可以自己嘗試實現一下。

 

 

 

 

 

 

 

 

幾點補充:

 

本漫畫純屬娛樂,還請大家儘量珍惜當下的工作,切勿模仿小灰的行爲哦。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章