共計 4296 個字符,預計需要花費 11 分鐘才能閱讀完成。
本文丸趣 TV 小編為大家詳細介紹“Redis 實現分布式鎖要注意哪些事項”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Redis 實現分布式鎖要注意哪些事項”文章能幫助大家解決疑惑,下面跟著丸趣 TV 小編的思路慢慢深入,一起來學習新知識吧。
Redis 實現分布式鎖
最近看分布式鎖的過程中看到一篇不錯的文章,特地的加工一番自己的理解:
Redis 分布式鎖實現的三個核心要素:
1. 加鎖
最簡單的方法是使用 setnx 命令。key 是鎖的唯一標識,按業務來決定命名,value 為當前線程的線程 ID。
比如想要給一種商品的秒殺活動加鎖,可以給 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 監聽了 Lock1,Client3 監聽了 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 實現分布式鎖要注意哪些事項”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注丸趣 TV 行業資訊頻道。