1. 程式人生 > 程式設計 >redis分散式鎖的實現(1)- 分散式鎖的設計理論

redis分散式鎖的實現(1)- 分散式鎖的設計理論

分散式鎖是什麼

分散式鎖是控制分散式系統或不同系統之間共同訪問共享資源的一種鎖實現 如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往通過互斥來防止彼此幹擾。

redis分散式鎖實現的三要素

加鎖

使用setnx命令加鎖,key是鎖的唯一標識,可以根據業務來命名,value為當前執行緒的ID或者UUID(後面介紹原因) 比如扣減商品庫存,key可是 lock_stock_upc ,value可以為當前執行緒ID。

  • setnx(key,value):
    • 當且僅當 key 不存在時,set 一個 key 為 val 的字串,返回 1,此時說明加鎖成功。
    • 若 key 存在,則什麼都不做,返回 【0】加鎖,此時說明加鎖失敗。

鎖超時

如果一個得到鎖的執行緒在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的執行緒再也別想進來。那麼這個鎖就永遠被無法獲取到 所以,我們需要給setnx的key必須設定一個超時時間,以保證在異常情況下即使鎖沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。

釋放鎖

當得到鎖的執行緒執行完任務,需要釋放鎖,以便其他執行緒可以進入。釋放鎖的最簡單方式是執行del指令,del(key)釋放鎖之後,其他執行緒就可以繼續執行setnx命令來獲得鎖。

實現分散式鎖的問題

死鎖

由於set和expire的非原子性,會導致一個異常情況

  • 伺服器宕機
    • 程式剛獲取到鎖,還沒有設定超時時間,這個時候伺服器宕機啦,那麼鎖沒來得及釋放,其他服務端永遠獲取不到鎖。
  • redis 宕機
    • redis獲取到鎖之後,還沒設定過期時間,redis服務掛了,那這個時候也會導致鎖無法被釋放,其他服務無法獲取到鎖

所以要保證SETNX和SETEX(設定過期時間)這2個命令一起執行,要麼都成功,要麼都失敗,保證其原子性。

誤刪其他執行緒的鎖

  • 假如某執行緒成功得到了鎖,並且設定的超時時間是30秒。如果某些原因導致執行緒B執行的很慢很慢,過了30秒都沒執行完,這時候鎖過期自動釋放,執行緒B得到了鎖。

  • 執行緒A執行完了任務,執行緒A接著執行del指令來釋放鎖。但這時候執行緒B還沒執行完,執行緒A實際上刪除的是執行緒B加的鎖。

怎麼避免這種情況呢?可以在del釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。

至於具體的實現,可以在加鎖的時候把當前的執行緒ID當做value,並在刪除之前驗證key對應的value是不是自己執行緒的ID。 這就是前面說的設定value的時候要設定成uuid或者執行緒ID的原因。

if(threadId .equals(redisClient.get(key))){
    del(key)
}
複製程式碼

然而由於,if判斷和釋放鎖是兩個獨立操作,不是原子性,所以採用Lua指令碼釋放鎖。後面介紹實現。

分散式鎖設計方案

通過以上可知,要想實現一個可靠的分散式鎖,設計鎖的時候需要考慮一下要素:

  1. 獲取鎖的時候,使用 setnx(SETNX key val:當且僅當 key 不存在時,set 一個 key 為 val 的字串,返回 1;
  2. 若 key 存在,則什麼都不做,返回 【0】加鎖,鎖的 value 值為當前佔有鎖伺服器內網IP編號拼接任務標識
  3. 在釋放鎖的時候進行判斷。並使用 expire 命令為鎖添 加一個超時時間,超過該時間則自動釋放鎖。 4 .返回1則成功獲取鎖。還設定一個獲取的超時時間, 若超過這個時間則放棄獲取鎖。setex(key,value,expire)過期以秒為單位 5 .釋放鎖的時候,判斷是不是該鎖(即Value為當前伺服器內網IP編號拼接任務標識),若是該鎖,則執行 delete 進行鎖釋放
  4. 設定鎖的時候要保證set 和expire(key, 30)這2個命令的原子性,
  5. 釋放鎖的時候要保證刪除的當前執行緒ID的鎖,要保證if(threadId .equals(redisClient.get(key))) 和 del(key)的原子性。
  6. setnx指令本身是不支援傳入超時時間的,Redis2.6.12以上版本為set指令增加了可選引數,虛擬碼如下:set(key,1,30,NX)

加鎖

SET key value NX PX 30000
value是由客戶端生成的一個隨機字串,相當於是客戶端持有鎖的標誌
NX表示只有key值不存在的時候才能SET成功,相當於只有第一個請求的客戶端才能獲得鎖
PX 30000表示這個鎖有一個30秒的自動過期時間。
複製程式碼

解鎖

為了防止客戶端1獲得的鎖,被客戶端2給釋放,採用下面的Lua指令碼來釋放鎖
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0
end
複製程式碼

redis分散式鎖的不靠譜

假如在redis sentinel叢集中,我們具有多臺redis,他們之間有著主從的關係,例如一主二從。我們的set命令對應的資料寫到主庫,然後同步到從庫。當我們申請一個鎖的時候,對應就是一條命令 setnx mykey myvalue ,在redis sentinel叢集中,這條命令先是落到了主庫。假設這時主庫down了,而這條資料還沒來得及同步到從庫,sentinel將從庫中的一臺選舉為主庫了。這時,我們的新主庫中並沒有mykey這條資料,若此時另外一個client執行 setnx mykey hisvalue,也會成功,即也能得到鎖。這就意味著,此時有兩個client獲得了鎖。這不是我們希望看到的,雖然這個情況發生的記錄很小,只會在主從failover的時候才會發生,大多數情況下、大多數系統都可以容忍,但是不是所有的系統都能容忍這種瑕疵。

redlock

為瞭解決故障轉移情況下的缺陷,Antirez 發明瞭 Redlock 演演算法,使用redlock演演算法,需要多個redis例項,加鎖的時候,它會想多半節點傳送 setex mykey myvalue 命令,只要過半節點成功了,那麼就算加鎖成功了。釋放鎖的時候需要想所有節點傳送del命令。這是一種基於【大多數都同意】的一種機制。我們可以選擇已有的開源實現,python有redlock-py,java 中有Redisson redlock。

redlock確實解決了上面所說的“不靠譜的情況”。但是,它解決問題的同時,也帶來了代價。你需要多個redis例項,你需要引入新的庫 程式碼也得調整,效能上也會有損。所以,果然是不存在“完美的解決方案”,我們更需要的是能夠根據實際的情況和條件把問題解決了就好。