使用 Redis 如何設計分布式鎖?


一、什么是分布式鎖?

要使用redis來設計分布式鎖,首先要了解什么是分布式鎖,而要了解什么是分布式鎖,先要提到與分布式鎖相對應的線程鎖和進程鎖。
線程鎖:線程鎖主要是用來給方法和代碼塊加鎖。當某個方法或者某段代碼使用線程鎖時,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一個JVM中有效果,因為線程鎖的實現根本上是依靠線程之間共享內存來實現的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。
進程鎖:為了控制同一個操作系統中多個進程訪問某個共享資源,因為進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖來實現進程鎖。
分布式鎖:當我們在某一生產環境中啟動多個訂單服務時,就是多個JVM,內存中的鎖顯然是不共享的,每個JVM進程都有自己的鎖,自然無法保證線程的互斥了,這個時候我們就需要使用到分布式鎖。也就是說,當多個進程不在同一個系統中時,我們用分布式鎖來控制多個進程對資源的訪問。

二、分布式鎖的使用場景

線程間的並發問題和進程間的並發問題都是可以通過分布式鎖來解決的,但是強烈不建議這么做!因為采用分布式鎖來解決這些小問題是非常消耗資源的!分布式鎖應該用來解決分布式情況下的多進程並發問題才是最合適的。

比如有這么一個場景,線程A和線程B都共享某個變量X。
如果是在單機情況下(即單JVM),線程之間共享內存,那么用線程鎖就能解決並發問題。
如果是在分布式情況下(即多JVM),線程A和線程B很可能不是在同一個JVM中,這樣線程鎖就無法使用了,這時候就需要用分布式鎖來解決。

實現分布式鎖常用的有三種解決方案:1.基於數據庫實現 2.基於zookeeper的臨時序列化節點實現 3.redis實現。本文我們介紹的就是redis的實現方式。

三、分布式鎖的實現

實現分布式鎖有 3 點需要注意:

  1. 互斥(即同一時刻只能有一個線程獲取到鎖)
  2. 不能死鎖,因此鎖信息必須是會過期超時的,不能讓一個線程長期占有鎖而導致死鎖
  3. 容錯(只要大部分 Redis 節點創建了這把鎖就可以)

幾個要用到的redis命令:

  1. SETNX(key,value)
  2. GET(key)
  3. GETSET(key,value)
  4. EXPIRE(key,seconds)

Redis官方給出了兩種基於Redis實現分布式鎖的方法。(點擊這里獲取詳情)

四、Redis最普通的分布式鎖(單Redis實例實現分布式鎖)

1. 加鎖
加鎖實際上就是,在redis中使用SET key value [EX seconds] [PX milliseconds] NX命令給Key鍵設置一個值,為了避免死鎖,還要給定一個過期時間。如執行以下命令:

SET lock_key random_value NX PX 5000

其中:
lock_key為resource_name。
random_value 為key的值(一個隨機值),這個值在所有的客戶端必須是唯一的,所有同一key的獲取者(競爭者)這個值都不能一樣。可以用snowflake算法生成分布式唯一id
NX表示只在key不存在時,才對key進行設置操作; PX 5000表示設置key的過期時間為5000毫秒。 即這個命令僅在不存在key的時候才能被執行成功(NX選項),並且這個key有一個5秒的自動失效時間(PX屬性)

這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。

2. 解鎖
解鎖即釋放鎖,解鎖的過程就是將key鍵給刪除。但是也不能亂刪,比如說有這么個場景,客戶端1先獲取到了鎖,但是阻塞了很長時間才執行完,比如說超過了30秒,這個時候可能已經自動釋放鎖了,此時客戶端2可能已經獲取到了鎖,這個時候如果直接刪除key的話肯定會出問題的,不能用客戶端1的請求來將客戶端2的鎖給刪除掉。這時候random_value的作用就體現出來了。可以用隨機值和LUA腳本對key進行刪除避免上述情況,因為腳本僅會刪除value等於客戶端1的value的key(value相當於客戶端的一個簽名)。

首先要判斷key是否存在並且存儲的值是否和傳入的值一樣,若是則刪除key,解鎖成功。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

單節點Redis的分布式鎖的實現比較簡單,但是也存在比較大的問題,最重要的一點是,鎖不具有可重入性。如果是Redis普通主從,那Redis主從異步復制,若主節點掛了的話(也就是key沒有了),此時key還沒同步到從節點,此時從節點切換到主節點,別人就可以set key,從而拿到鎖,會造成比較嚴重的安全問題。

五、RedLock算法

這個場景是假設有一個Redis集群,有5個Redis master節點,這些節點完全互相獨立,不存在主從復制或者其他集群協調機制。之前我們已經描述了在Redis單實例下怎么安全地獲取和釋放鎖。這里我們確保將在每(5)個實例上使用此方法獲取和釋放鎖,我們需要在5台機器上面或者5台虛擬機上面運行這些實例,這樣保證他們不會同時都宕掉。

為了取到鎖,客戶端應該執行以下操作:

  1. 獲取當前的時間戳,以毫秒為單位。
  2. 與上面類似,依次輪流嘗試從5個實例,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該盡快嘗試另外一個Redis實例。
  3. 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,因此這里是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。
  4. 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
  5. 如果因為某些原因,獲取鎖失敗(沒有在至少(N/2+1)個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功),否則影響其他客戶端獲取鎖,無論Redis實例是否加鎖成功,因為可能服務端響應消息丟失了但是實際成功了,畢竟多釋放一次也不會有問題。

這個算法是異步的嗎?

算法基於這樣一個假設:雖然多個進程之間沒有時鍾同步,但每個進程都以相同的時鍾頻率前進,時間差相對於失效時間來說幾乎可以忽略不計。這種假設和我們的真實世界非常接近:每個計算機都有一個本地時鍾,我們可以容忍多個計算機之間有較小的時鍾漂移。
從這點來說,我們必須再次強調我們的互相排斥規則:只有在鎖的有效時間(在步驟3計算的結果)范圍內客戶端能夠做完它的工作,鎖的安全性才能得到保證(鎖的實際有效時間通常要比設置的短,因為計算機之間有時鍾漂移的現象)。

失敗時重試

當客戶端無法取到鎖時,應該在一個隨機延遲后重試,防止多個客戶端在同時搶奪同一資源的鎖(這樣會導致腦裂,沒有人會取到鎖)。同樣,客戶端取得大部分Redis實例鎖所花費的時間越短,腦裂出現的概率就會越低(必要的重試),所以,理想情況一下,客戶端應該同時(並發地)向所有Redis發送SET命令。
需要強調,當客戶端從大多數Redis實例獲取鎖失敗時,應該盡快地釋放(部分)已經成功取到的鎖,這樣其他的客戶端就不必非得等到鎖過完“有效時間”才能取到(然而,如果已經存在網絡分裂,客戶端已經無法和Redis實例通信,此時就只能等待key的自動釋放了,等於被懲罰了)。

釋放鎖

釋放鎖比較簡單,向所有的Redis實例發送釋放鎖命令即可,不用關心之前有沒有從Redis實例成功獲取到鎖。

詳情查看:
Redis中國官方網站-Redis分布式鎖

關於Redlock算法是否安全的爭論

2016年2月8號分布式系統的專家馬丁·克萊普曼博士(Martin Kleppmann)在一篇文章How to do distributed locking 指出分布式鎖設計的一些原則並且對Antirez的Redlock算法提出了一些質疑。
Martin指出:

  1. 即使我們擁有一個完美實現的分布式鎖,在沒有共享資源參與進來提供某種fencing柵欄機制的前提下,我們仍然不可能獲得足夠的安全性
  2. 由於Redlock本質上是建立在一個同步模型之上,對系統的時間有很強的要求,本身的安全性是不夠的

針對第1點,Martin認為,獲取鎖的客戶端在持有鎖時可能會暫停一段較長的時間,盡管鎖有一個超時時間,避免了崩潰的客戶端可能永遠持有鎖並且永遠不會釋放它,但是如果客戶端的暫停持續的時間長於鎖的到期時間,並且客戶沒有意識到它已經到期,那么它可能會繼續進行一些不安全的更改,換言之由於客戶端阻塞導致的持有的鎖到期而不自知
對於這種情況馬丁指出要增加fencing機制,具體來說是fencing token隔離令牌機制。他給了個例子:
假設客戶端1獲得鎖並且獲得序號為33的令牌,但隨后它進入長時間暫停,直至鎖超時過期,客戶端2獲取鎖並且獲得序號為34的令牌,然后將其寫入發送到存儲服務。隨后,客戶端1復活並將其寫入發送到存儲服務,然而存儲服務器記得它已經處理了具有較高令牌號34的寫入,因此它拒絕令牌33的請求。Redlock算法並沒有這種唯一且遞增的fencing token生成機制,這也意味着Redlock算法不能避免由於客戶端阻塞帶來的鎖過期后的操作問題,因此是不安全的。

針對第2點,Martin認為,Redlock是個強依賴系統時間的算法,這樣就可能帶來很多不一致問題。他同樣給出了個例子:

假設多節點Redis系統有五個節點A/B/C/D/E和兩個客戶端C1和C2,如果其中一個Redis節點上的時鍾向前跳躍會發生什么?

  • 客戶端C1獲得了對節點A、B、C的鎖定,由於網絡問題,無法到達節點D和節點E
  • 節點C上的時鍾向前跳,導致鎖提前過期
  • 客戶端C2在節點C、D、E上獲得鎖定,由於網絡問題,無法到達A和B
  • 客戶端C1和客戶端C2現在都認為他們自己持有鎖

分布式異步模型:
上面這種情況之所以有可能發生,本質上是因為Redlock的安全性對Redis節點系統時鍾有強依賴,一旦系統時鍾變得不准確,算法的安全性也就無法保證。

馬丁其實是要指出分布式算法研究中的一些基礎性問題,好的分布式算法應該基於異步模型,算法的安全性不應該依賴於任何記時假設

分布式異步模型中進程和消息可能會延遲任意長的時間,系統時鍾也可能以任意方式出錯。這些因素不應該影響它的安全性,只可能影響到它的活性,即使在非常極端的情況下,算法最多是不能在有限的時間內給出結果,而不應該給出錯誤的結果,這樣的算法在現實中是存在的比如Paxos/Raft,按這個標准衡量Redlock的安全級別是達不到的。

對此,Redlock算法作者Antirez進行了反駁:地址
Antirez認為馬丁的文章對於Redlock的批評可以概括為兩個方面:

  • 帶有自動過期功能的分布式鎖,必須提供某種fencing柵欄機制來保證對共享資源的真正互斥保護,Redlock算法提供不了這樣一種機制
  • Redlock算法構建在一個不夠安全的系統模型之上,它對於系統的記時假設有比較強的要求,而這些要求在現實的系統中是無法保證的

Antirez對這兩方面分別進行了細致的反駁。

關於fencing機制

Antirez提出了質疑:既然在鎖失效的情況下已經存在一種fencing機制能繼續保持資源的互斥訪問了,那為什么還要使用一個分布式鎖並且還要求它提供那么強的安全性保證呢?
退一步講Redlock雖然提供不了遞增的fencing token隔離令牌,但利用Redlock產生的隨機字符串可以達到同樣的效果,這個隨機字符串雖然不是遞增的,但卻是唯一的。

關於記時假設

Antirez針對算法在記時模型假設集中反駁,馬丁認為Redlock失效情況主要有三種:

  1. 時鍾發生跳躍
  2. 長時間的GC pause
  3. 長時間的網絡延遲

后兩種情況來說,Redlock在當初之處進行了相關設計和考量,對這兩種問題引起的后果有一定的抵抗力。
時鍾跳躍對於Redlock影響較大,這種情況一旦發生Redlock是沒法正常工作的。
Antirez指出Redlock對系統時鍾的要求並不需要完全精確,只要誤差不超過一定范圍不會產生影響,在實際環境中是完全合理的,通過恰當的運維完全可以避免時鍾發生大的跳動

總結

分布式系統本身就很復雜,機制和理論的效果需要一定的數學推導作為依據,馬丁和Antirez都是這個領域的專家,對於一些問題都會有自己的看法和思考,更重要的是很多時候問題本身並沒有完美的解決方案,只有站在巨人的肩膀上才能做出更好的成績。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM