歡迎光臨
每天分享高質量文章

漫畫:什麼是分散式鎖?

來自:程式員小灰(微訊號:chengxuyuanxiaohui)

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





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



分散式鎖的實現有哪些?


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秒。


如果某些原因導致執行緒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)

}


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


我們都是追求極致的程式員,所以這一塊要用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和守護執行緒在同一個行程,守護執行緒也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。


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

幾點補充:


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


●編號367,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

 

Web開發

更多推薦:18個技術類微信公眾號

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂