使用Redis做爲分佈式鎖的一些注意點

Redis實現分佈式鎖

最近看分佈式鎖的過程當中看到一篇不錯的文章,特意的加工一番本身的理解:java

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

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命令來得到鎖。lua

 

3.鎖超時spa

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

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

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

 

if(setnx(key,1) == 1){
    expire(key,30try {
        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秒。

若是某些緣由致使線程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)

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分佈式鎖的優缺點:

 

相關文章
相關標籤/搜索