基於setnx和getsetphp
http://blog.csdn.net/lihao21/article/details/49104695html
使用Redis的 SETNX 命令能夠實現分佈式鎖,下文介紹其實現方法。node
SETNX key valuepython
將 key 的值設爲 value,當且僅當 key 不存在。
若給定的 key 已經存在,則 SETNX 不作任何動做。
SETNX 是SET if Not eXists的簡寫。git
返回整數,具體爲
- 1,當 key 的值被設置
- 0,當 key 的值沒被設置github
redis> SETNX mykey 「hello」
(integer) 1
redis> SETNX mykey 「hello」
(integer) 0
redis> GET mykey
「hello」
redis>redis
多個進程執行如下Redis命令:算法
SETNX lock.foo <current Unix time + lock timeout + 1>
編程
若是 SETNX 返回1,說明該進程得到鎖,SETNX將鍵 lock.foo 的值設置爲鎖的超時時間(當前時間 + 鎖的有效時間)。
若是 SETNX 返回0,說明其餘進程已經得到了鎖,進程不能進入臨界區。進程能夠在一個循環中不斷地嘗試 SETNX 操做,以得到鎖。安全
考慮一種狀況,若是進程得到鎖後,斷開了與 Redis 的鏈接(多是進程掛掉,或者網絡中斷),若是沒有有效的釋放鎖的機制,那麼其餘進程都會處於一直等待的狀態,即出現「死鎖」。
上面在使用 SETNX 得到鎖時,咱們將鍵 lock.foo 的值設置爲鎖的有效時間,進程得到鎖後,其餘進程還會不斷的檢測鎖是否已超時,若是超時,那麼等待的進程也將有機會得到鎖。
然而,鎖超時時,咱們不能簡單地使用 DEL 命令刪除鍵 lock.foo 以釋放鎖。考慮如下狀況,進程P1已經首先得到了鎖 lock.foo,而後進程P1掛掉了。進程P2,P3正在不斷地檢測鎖是否已釋放或者已超時,執行流程以下:
從上面的狀況能夠得知,在檢測到鎖超時後,進程不能直接簡單地執行 DEL 刪除鍵的操做以得到鎖。
爲了解決上述算法可能出現的多個進程同時得到鎖的問題,咱們再來看如下的算法。
咱們一樣假設進程P1已經首先得到了鎖 lock.foo,而後進程P1掛掉了。接下來的狀況:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
另外,值得注意的是,在進程釋放鎖,即執行 DEL lock.foo 操做前,須要先判斷鎖是否已超時。若是鎖已超時,那麼鎖可能已由其餘進程得到,這時直接執行 DEL lock.foo 操做會致使把其餘進程已得到的鎖釋放掉。
用如下python代碼來實現上述的使用 SETNX 命令做分佈式鎖的算法。
LOCK_TIMEOUT = 3 lock = 0 lock_timeout = 0 lock_key = 'lock.foo' # 獲取鎖 while lock != 1: now = int(time.time()) lock_timeout = now + LOCK_TIMEOUT + 1 lock = redis_client.setnx(lock_key, lock_timeout) if lock == 1 or (now > int(redis_client.get(lock_key))) and now > int(redis_client.getset(lock_key, lock_timeout)): break else: time.sleep(0.001) # 已得到鎖 do_job() # 釋放鎖 now = int(time.time()) if now < lock_timeout: redis_client.delete(lock_key)
在不一樣進程須要互斥地訪問共享資源時,分佈式鎖是一種很是有用的技術手段。 有不少三方庫和文章描述如何用Redis實現一個分佈式鎖管理器,可是這些庫實現的方式差異很大,並且不少簡單的實現其實只需採用稍微增長一點複雜的設計就能夠得到更好的可靠性。 這篇文章的目的就是嘗試提出一種官方權威的用Redis實現分佈式鎖管理器的算法,咱們把這個算法稱爲RedLock,咱們相信這個算法會比通常的普通方法更加安全可靠。咱們也但願社區能一塊兒分析這個算法,提供一些反饋,而後咱們以此爲基礎,來設計出更加複雜可靠的算法,或者 更好的新算法。
實現
在描述具體的算法以前,下面是已經實現了的項目能夠做爲參考: Redlock-rb (Ruby實現)。還有一個Redlock-rb的分支,添加了一些特性使得實現分佈式鎖更簡單
在描述咱們的設計以前,咱們想先提出三個屬性,這三個屬性在咱們看來,是實現高效分佈式鎖的基礎。
爲了理解咱們想要提升的究竟是什麼,咱們先看下當前大多數基於Redis的分佈式鎖三方庫的現狀。 用Redis來實現分佈式鎖最簡單的方式就是在實例裏建立一個鍵值,建立出來的鍵值通常都是有一個超時時間的(這個是Redis自帶的超時特性),因此每一個鎖最終都會釋放(參見前文屬性2)。而當一個客戶端想要釋放鎖時,它只須要刪除這個鍵值便可。 表面來看,這個方法彷佛很管用,可是這裏存在一個問題:在咱們的系統架構裏存在一個單點故障,若是Redis的master節點宕機了怎麼辦呢?有人可能會說:加一個slave節點!在master宕機時用slave就好了!可是其實這個方案明顯是不可行的,由於這種方案沒法保證第1個安全互斥屬性,由於Redis的複製是異步的。 總的來講,這個方案裏有一個明顯的競爭條件(race condition),舉例來講:
固然,在某些特殊場景下,前面提到的這個方案則徹底沒有問題,好比在宕機期間,多個客戶端容許同時都持有鎖,若是你能夠容忍這個問題的話,那用這個基於複製的方案就徹底沒有問題,不然的話咱們仍是建議你採用這篇文章裏接下來要描述的方案。
在講述如何用其餘方案突破單實例方案的限制以前,讓咱們先看下是否有什麼辦法能夠修復這個簡單場景的問題,由於這個方案其實若是能夠忍受競爭條件的話是有望可行的,並且單實例來實現分佈式鎖是咱們後面要講的算法的基礎。 要得到鎖,要用下面這個命令: SET resource_name my_random_value NX PX 30000 這個命令的做用是在只有這個key不存在的時候纔會設置這個key的值(NX選項的做用),超時時間設爲30000毫秒(PX選項的做用) 這個key的值設爲「my_random_value」。這個值必須在全部獲取鎖請求的客戶端裏保持惟一。 基本上這個隨機值就是用來保證能安全地釋放鎖,咱們能夠用下面這個Lua腳原本告訴Redis:刪除這個key當且僅當這個key存在並且值是我指望的那個值。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
這個很重要,由於這能夠避免誤刪其餘客戶端獲得的鎖,舉個例子,一個客戶端拿到了鎖,被某個操做阻塞了很長時間,過了超時時間後自動釋放了這個鎖,而後這個客戶端以後又嘗試刪除這個其實已經被其餘客戶端拿到的鎖。因此單純的用DEL指令有可能形成一個客戶端刪除了其餘客戶端的鎖,用上面這個腳本能夠保證每一個客戶單都用一個隨機字符串’簽名’了,這樣每一個鎖就只能被得到鎖的客戶端刪除了。
這個隨機字符串應該用什麼生成呢?我假設這是從/dev/urandom生成的20字節大小的字符串,可是其實你能夠有效率更高的方案來保證這個字符串足夠惟一。好比你能夠用RC4加密算法來從/dev/urandom生成一個僞隨機流。還有更簡單的方案,好比用毫秒的unix時間戳加上客戶端id,這個也許不夠安全,可是也許在大多數環境下已經夠用了。
key值的超時時間,也叫作」鎖有效時間」。這個是鎖的自動釋放時間,也是一個客戶端在其餘客戶端能搶佔鎖以前能夠執行任務的時間,這個時間從獲取鎖的時間點開始計算。 因此如今咱們有很好的獲取和釋放鎖的方式,在一個非分佈式的、單點的、保證永不宕機的環境下這個方式沒有任何問題,接下來咱們看看沒法保證這些條件的分佈式環境下咱們該怎麼作。
在分佈式版本的算法裏咱們假設咱們有N個Redis master節點,這些節點都是徹底獨立的,咱們不用任何複製或者其餘隱含的分佈式協調算法。咱們已經描述瞭如何在單節點環境下安全地獲取和釋放鎖。所以咱們理所固然地應當用這個方法在每一個單節點裏來獲取和釋放鎖。在咱們的例子裏面咱們把N設成5,這個數字是一個相對比較合理的數值,所以咱們須要在不一樣的計算機或者虛擬機上運行5個master節點來保證他們大多數狀況下都不會同時宕機。一個客戶端須要作以下操做來獲取鎖:
1.獲取當前時間(單位是毫秒)。
2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裏,客戶端在每一個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。好比若是鎖自動釋放時間是10秒鐘,那每一個節點鎖請求的超時時間多是5-50毫秒的範圍,這個能夠防止一個客戶端在某個宕掉的master節點上阻塞過長時間,若是一個master節點不可用了,咱們應該儘快嘗試下一個master節點。
3.客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裏是3個),並且總共消耗的時間不超過鎖釋放時間,這個鎖就認爲是獲取成功了。
4.若是鎖獲取成功了,那如今鎖自動釋放時間就是最初的鎖釋放時間減去以前獲取鎖所消耗的時間。
5.若是鎖獲取失敗了,無論是由於獲取成功的鎖不超過一半(N/2+1)仍是由於總消耗時間超過了鎖釋放時間,客戶端都會到每一個master節點上釋放鎖,即使是那些他認爲沒有獲取成功的鎖。
這個算法是基於一個假設:雖然不存在能夠跨進程的同步時鐘,可是不一樣進程時間都是以差很少相同的速度前進,這個假設不必定徹底準確,可是和自動釋放鎖的時間長度相比不一樣進程時間前進速度差別基本是能夠忽略不計的。這個假設就比如真實世界裏的計算機:每一個計算機都有本地時鐘,可是咱們能夠說大部分狀況下不一樣計算機之間的時間差是很小的。 如今咱們須要更細化咱們的鎖互斥規則,只有當客戶端能在T時間內完成所作的工做才能保證鎖是有效的(詳見算法的第3步),T的計算規則是鎖失效時間T1減去一個用來補償不一樣進程間時鐘差別的delta值(通常只有幾毫秒而已) 若是想了解更多基於有限時鐘差別的相似系統,能夠參考這篇有趣的文章:《Leases: an efficient fault-tolerant mechanism for distributed file cache consistency.》
當一個客戶端獲取鎖失敗時,這個客戶端應該在一個隨機延時後進行重試,之因此採用隨機延時是爲了不不一樣客戶端同時重試致使誰都沒法拿到鎖的狀況出現。一樣的道理客戶端越快嘗試在大多數Redis節點獲取鎖,出現多個客戶端同時競爭鎖和重試的時間窗口越小,可能性就越低,因此最完美的狀況下,客戶端應該用多路傳輸的方式同時向全部Redis節點發送SET命令。 這裏很是有必要強調一下客戶端若是沒有在多數節點獲取到鎖,必定要儘快在獲取鎖成功的節點上釋放鎖,這樣就不必等到key超時後才能從新獲取這個鎖(可是若是網絡分區的狀況發生並且客戶端沒法鏈接到Redis節點時,會損失等待key超時這段時間的系統可用性)
釋放鎖比較簡單,由於只須要在全部節點都釋放鎖就行,無論以前有沒有在該節點獲取鎖成功。
這個算法究竟是不是安全的呢?咱們能夠觀察不一樣場景下的狀況來理解這個算法爲何是安全的。 開始以前,讓咱們假設客戶端能夠在大多數節點都獲取到鎖,這樣全部的節點都會包含一個有相同存活時間的key。可是須要注意的是,這個key是在不一樣時間點設置的,因此這些key也會在不一樣的時間超時,可是咱們假設最壞狀況下第一個key是在T1時間設置的(客戶端鏈接到第一個服務器時的時間),最後一個key是在T2時間設置的(客戶端收到最後一個服務器返回結果的時間),從T2時間開始,咱們能夠確認最先超時的key至少也會存在的時間爲MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT,TTL是鎖超時時間、(T2-T1)是最晚獲取到的鎖的耗時,CLOCK_DRIFT是不一樣進程間時鐘差別,這個是用來補償前面的(T2-T1)。其餘的key都會在這個時間點以後纔會超時,因此咱們能夠肯定這些key在這個時間點以前至少都是同時存在的。
在大多數節點的key都set了的時間段內,其餘客戶端沒法搶佔這個鎖,由於在N/2+1個客戶端的key已經存在的狀況下不可能再在N/2+1個客戶端上獲取鎖成功,因此若是一個鎖獲取成功了,就不可能同時從新獲取這個鎖成功(否則就違反了分佈式鎖互斥原則),而後咱們也要確保多個客戶端同時嘗試獲取鎖時不會都同時成功。 若是一個客戶端獲取大多數節點鎖的耗時接近甚至超過鎖的最大有效時間時(就是咱們爲SET操做設置的TTL值),那麼系統會認爲這個鎖是無效的同時會釋放這些節點上的鎖,因此咱們僅僅須要考慮獲取大多數節點鎖的耗時小於有效時間的狀況。在這種狀況下,根據咱們前面的證實,在MIN_VALIDITY時間內,沒有客戶端能從新獲取鎖成功,因此多個客戶端都能同時成功獲取鎖的結果,只會發生在多數節點獲取鎖的時間都大大超過TTL時間的狀況下,實際上這種狀況下這些鎖都會失效 。 咱們很是期待和歡迎有人能提供這個算法安全性的公式化證實,或者發現任何bug。
這個系統的性能主要基於如下三個主要特徵:
1.鎖自動釋放的特徵(超時後會自動釋放),必定時間後某個鎖都能被再次獲取。
2.客戶端一般會在再也不須要鎖或者任務執行完成以後主動釋放鎖,這樣咱們就不用等到超時時間會再去獲取這個鎖。
3.當一個客戶端須要重試獲取鎖時,這個客戶端會等待一段時間,等待的時間相對來講會比咱們從新獲取大多數鎖的時間要長一些,這樣能夠下降不一樣客戶端競爭鎖資源時發生死鎖的機率。
然而,咱們在網絡分區時要損失TTL的可用性時間,因此若是網絡分區持續發生,這個不可用會一直持續。這種狀況在每次一個客戶端獲取到了鎖並在釋放鎖以前被網絡分區了時都會出現。
基原本說,若是持續的網絡分區發生的話,系統也會在持續不可用。
不少使用Redis作鎖服務器的用戶在獲取鎖和釋放鎖時不止要求低延時,同時要求高吞吐量,也即單位時間內能夠獲取和釋放的鎖數量。爲了達到這個要求,必定會使用多路傳輸來和N個服務器進行通訊以下降延時(或者也能夠用假多路傳輸,也就是把socket設置成非阻塞模式,發送全部命令,而後再去讀取返回的命令,假設說客戶端和不一樣Redis服務節點的網絡往返延時相差不大的話)。
而後若是咱們想讓系統能夠自動故障恢復的話,咱們還須要考慮一下信息持久化的問題。
爲了更好的描述問題,咱們先假設咱們Redis都是配置成非持久化的,某個客戶端拿到了總共5個節點中的3個鎖,這三個已經獲取到鎖的節點中隨後重啓了,這樣一來咱們又有3個節點能夠獲取鎖了(重啓的那個加上另外兩個),這樣一來其餘客戶端又能夠得到這個鎖了,這樣就違反了咱們以前說的鎖互斥原則了。
若是咱們啓用AOF持久化功能,狀況會好不少。舉例來講,咱們能夠發送SHUTDOWN命令來升級一個Redis服務器而後重啓之,由於Redis超時時效是語義層面實現的,因此在服務器關掉期間時超時時間仍是算在內的,咱們全部要求仍是知足了的。而後這個是基於咱們作的是一次正常的shutdown,可是若是是斷電這種意外停機呢?若是Redis是默認地配置成每秒在磁盤上執行一次fsync同步文件到磁盤操做,那就可能在一次重啓後咱們鎖的key就丟失了。理論上若是咱們想要在全部服務重啓的狀況下都確保鎖的安全性,咱們須要在持久化設置裏設置成永遠執行fsync操做,可是這個反過來又會形成性能遠不如其餘同級別的傳統用來實現分佈式鎖的系統。 而後問題其實並不像咱們第一眼看起來那麼糟糕,基本上只要一個服務節點在宕機重啓後不去參與如今全部仍在使用的鎖,這樣正在使用的鎖集合在這個服務節點重啓時,算法的安全性就能夠維持,由於這樣就能夠保證正在使用的鎖都被全部沒重啓的節點持有。 爲了知足這個條件,咱們只要讓一個宕機重啓後的實例,至少在咱們使用的最大TTL時間內處於不可用狀態,超過這個時間以後,全部在這期間活躍的鎖都會自動釋放掉。 使用延時重啓的策略基本上能夠在不適用任何Redis持久化特性狀況下保證安全性,而後要注意這個也必然會影響到系統的可用性。舉個例子,若是系統裏大多數節點都宕機了,那在TTL時間內整個系統都處於全局不可用狀態(全局不可用的意思就是在獲取不到任何鎖)。
若是客戶端作的工做都是由一些小的步驟組成,那麼就有可能使用更小的默認鎖有效時間,並且擴展這個算法來實現一個鎖擴展機制。基本上,客戶端若是在執行計算期間發現鎖快要超時了,客戶端能夠給全部服務實例發送一個Lua腳本讓服務端延長鎖的時間,只要這個鎖的key還存在並且值還等於客戶端獲取時的那個值。 客戶端應當只有在失效時間內沒法延長鎖時再去從新獲取鎖(基本上這個和獲取鎖的算法是差很少的) 然而這個並不會對從本質上改變這個算法,因此最大的從新獲取鎖數量應該被設置成合理的大小,否則性能必然會受到影響。
若是你很瞭解分佈式系統的話,咱們很是歡迎你提供一些意見和分析。固然若是能引用其餘語言的實現話就更棒了。 謝謝!
原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com