Redis實現分散式鎖需要注意什麼?【注意事項總結】

2022-03-04 19:00:57
Redis實現分散式鎖需要注意什麼?下面本篇文章就來給大家總結分享一些使用Redis作為分散式鎖的注意點,希望對大家有所幫助!

Redis實現分散式鎖

最近看分散式鎖的過程中看到一篇不錯的文章,特地的加工一番自己的理解:

Redis分散式鎖實現的三個核心要素:

1.加鎖

最簡單的方法是使用setnx命令。key是鎖的唯一標識,按業務來決定命名,value為當前執行緒的執行緒ID。【相關推薦:Redis視訊教學

比如想要給一種商品的秒殺活動加鎖,可以給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 ......
    }catch()  {  }  finally {
       del(key)
    }

}

因為上面的虛擬碼中,存在著三個致命問題:

1. setnx和expire的非原子性

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

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

if(setnx(key,1) == 1){  //此處掛掉了.....
    expire(key,30)
    try {
        do something ......
    }catch()
  {
  }
  finally {
       del(key)
    }
 
}

這樣一來,這把鎖就沒有設定過期時間,變得「長生不老」,別的執行緒再也無法獲得鎖了。

怎麼解決呢?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)
doSomething.....
 
解鎖:
if(threadId .equals(redisClient.get(key))){
    del(key)
}

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

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

memcache實現分散式鎖

首頁top 10, 由資料庫載入到memcache快取n分鐘
微博中名人的content cache, 一旦不存在會大量請求不能命中並載入資料庫
需要執行多個IO操作生成的資料存在cache中, 比如查詢db多次
問題
在大並行的場合,當cache失效時,大量並行同時取不到cache,會同一瞬間去存取db並回設cache,可能會給系統帶來潛在的超負荷風險。我們曾經線上上系統出現過類似故障。

解決方法

if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
 
sleep(50);
retry();
}
}

在load db之前先add一個mutex key, mutex key add成功之後再去做載入db, 如果add失敗則sleep之後重試讀取原cache資料。為了防止死鎖,mutex key也需要設定過期時間。虛擬碼如下

Zookeeper實現分散式快取

Zookeeper的資料儲存結構就像一棵樹,這棵樹由節點組成,這種節點叫做Znode

Znode分為四種型別:

  • 1.持久節點 (PERSISTENT)

預設的節點型別。建立節點的使用者端與zookeeper斷開連線後,該節點依舊存在 。

  • 2.持久節點順序節點(PERSISTENT_SEQUENTIAL)

所謂順序節點,就是在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號:

  • 3.臨時節點(EPHEMERAL)

和持久節點相反,當建立節點的使用者端與zookeeper斷開連線後,臨時節點會被刪除:

  • 4.臨時順序節點(EPHEMERAL_SEQUENTIAL)

顧名思義,臨時順序節點結合和臨時節點和順序節點的特點:在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號;當建立節點的使用者端與zookeeper斷開連線後,臨時節點會被刪除。

Zookeeper分散式鎖恰恰應用了臨時順序節點。具體如何實現呢?讓我們來看一看詳細步驟:

  • 獲取鎖

首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個使用者端想要獲得鎖時,需要在ParentLock這個節點下面建立一個臨時順序節點 Lock1

之後,Client1查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock1是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。

這時候,如果再有一個使用者端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2

Client2查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock2是不是順序最靠前的一個,結果發現節點Lock2並不是最小的。

於是,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味著Client2搶鎖失敗,進入了等待狀態。

這時候,如果又有一個使用者端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3

Client3查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock3是不是順序最靠前的一個,結果同樣發現節點Lock3並不是最小的。

於是,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味著Client3同樣搶鎖失敗,進入了等待狀態。

這樣一來,Client1得到了鎖,Client2監聽了Lock1Client3監聽了Lock2。這恰恰形成了一個等待佇列,很像是Java當中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)

  • 釋放鎖

釋放鎖分為兩種情況:

1.任務完成,使用者端顯示釋放

當任務完成時,Client1會顯示呼叫刪除節點Lock1的指令。

2.任務執行過程中,使用者端崩潰

獲得鎖的Client1在任務執行過程中,如果Duang的一聲崩潰,則會斷開與Zookeeper伺服器端的連結。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。

由於Client2一直監聽著Lock1的存在狀態,當Lock1節點被刪除,Client2會立刻收到通知。這時候Client2會再次查詢ParentLock下面的所有節點,確認自己建立的節點Lock2是不是目前最小的節點。如果是最小,則Client2順理成章獲得了鎖。

同理,如果Client2也因為任務完成或者節點崩潰而刪除了節點Lock2,那麼Cient3就會接到通知。

最終,Client3成功得到了鎖。

Zookeeper和Redis分散式鎖的比較

下面的表格總結了Zookeeper和Redis分散式鎖的優缺點:

更多程式設計相關知識,請存取:!!

以上就是Redis實現分散式鎖需要注意什麼?【注意事項總結】的詳細內容,更多請關注TW511.COM其它相關文章!