關於Redis實現分佈式鎖的問題,網絡上不少,可是不少人的討論基本就是把原來博主的貼過來,甚至不少面試官也是隻知其一;不知其二經不起推敲就來面候選人,最近結合我本身的學習和資料查閱,整理一下用Redis實現分佈式鎖的方法,歡迎評論、交流、討論。html
獲取鎖的過程很簡單,客戶端向Redis發送命令:面試
SET resource_name my_random_value NX PX 30000
my_random_value
是由客戶端生成的一個隨機字符串,它要保證在足夠長的一段時間內在全部客戶端的全部獲取鎖的請求中都是惟一的。
NX表示只有當resource_name
對應的key值不存在的時候才能SET成功。這保證了只有第一個請求的客戶端才能得到鎖,而其它客戶端在鎖被釋放以前都沒法得到鎖。
PX 30000表示這個鎖有一個30秒的自動過時時間。redis
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
以前獲取鎖的時候生成的my_random_value
做爲參數傳到Lua腳本里面,做爲:ARGV[1]
,而 resource_name
做爲KEYS[1]
。Lua腳本能夠保證操做的原子性。算法
網絡上有文章說用以下命令獲取鎖:數據庫
SETNX resource_name my_random_value EXPIRE resource_name 30
因爲這兩個命令不是原子的。若是客戶端在執行完SETNX
後crash
了,那麼就沒有機會執行EXPIRE
了,致使它一直持有這個鎖,其餘的客戶端就永遠獲取不到這個鎖了。安全
my_random_value
要設置成隨機值?保證了一個客戶端釋放的鎖是本身持有的那個鎖。如若否則,可能出現鎖不安全的狀況。網絡
客戶端1獲取鎖成功。 客戶端1在某個操做上阻塞了很長時間。 過時時間到了,鎖自動釋放了。 客戶端2獲取到了對應同一個資源的鎖。 客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。
網上大量文章說用以下命令獲取鎖:dom
SETNX lock.foo <current Unix time + lock timeout + 1>
原文在Redis對SETNX的官網說明,Redis官網文檔建議用Set命令來代替,主要緣由是SETNX不支持超時時間的設置。分佈式
https://redis.io/commands/setnx學習
上面的討論中咱們有一個很是重要的假設:Redis是單點的。若是Redis是集羣模式,咱們考慮以下場景:
客戶端1從Master獲取了鎖。 Master宕機了,存儲鎖的key尚未來得及同步到Slave上。 Slave升級爲Master。 客戶端2重新的Master獲取到了對應同一個資源的鎖。 客戶端1和客戶端2同時持有了同一個資源的鎖,鎖再也不具備安全性。
就此問題,Redis做者antirez寫了RedLock算法來解決這種問題。
客戶端向全部Redis節點發起釋放鎖的操做,無論這些節點當時在獲取鎖的時候成功與否。
假設一共有5個Redis節點:A, B, C, D, E。設想發生了以下的事件序列:
客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。 節點C崩潰重啓了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。 節點C重啓後,客戶端2鎖住了C, D, E,獲取鎖成功。 客戶端1和客戶端2同時得到了鎖。
爲了應對這一問題,antirez又提出了延遲重啓(delayed restarts)的概念。也就是說,一個節點崩潰後,先不當即重啓它,而是等待一段時間再重啓,這段時間應該大於鎖的有效時間(lock validity time)。這樣的話,這個節點在重啓前所參與的鎖都會過時,它在重啓後就不會對現有的鎖形成影響。
解釋一下這個時序圖,客戶端1在得到鎖以後發生了很長時間的GC pause,在此期間,它得到的鎖過時了,而客戶端2得到了鎖。當客戶端1從GC pause中恢復過來的時候,它不知道本身持有的鎖已通過期了,它依然向共享資源(上圖中是一個存儲服務)發起了寫數據請求,而這時鎖實際上被客戶端2持有,所以兩個客戶端的寫請求就有可能衝突(鎖的互斥做用失效了)。
如何解決這個問題呢?引入了fencing token的概念:
客戶端1先獲取到的鎖,所以有一個較小的fencing token,等於33,而客戶端2後獲取到的鎖,有一個較大的fencing token,等於34。客戶端1從GC pause中恢復過來以後,依然是向存儲服務發送訪問請求,可是帶了fencing token = 33。存儲服務發現它以前已經處理過34的請求,因此會拒絕掉此次33的請求。這樣就避免了衝突。
可是其實這已經超出了Redis實現分佈式鎖的範圍,單純用Redis沒有命令來實現生成Token。
假設有5個Redis節點A, B, C, D, E。
客戶端1從Redis節點A, B, C成功獲取了鎖(多數節點)。因爲網絡問題,與D和E通訊失敗。 節點C上的時鐘發生了向前跳躍,致使它上面維護的鎖快速過時。 客戶端2從Redis節點C, D, E成功獲取了同一個資源的鎖(多數節點)。 客戶端1和客戶端2如今都認爲本身持有了鎖。 這個問題用Redis實現分佈式鎖暫時無解。而生產環境這種狀況是存在的。
結論
Redis並不能實現嚴格意義上的分佈式鎖。可是這並不意味着上面討論的方案一無可取。若是你的應用場景爲了效率(efficiency),協調各個客戶端避免作重複的工做,即便鎖失效了,只是可能把某些操做多作一遍而已,不會產生其它的不良後果。可是若是你的應用場景是爲了正確性(correctness),那麼用Redis實現分佈式鎖並不合適,會存在各類各樣的問題,且解決起來就很複雜,爲了正確性,須要使用zab、raft共識算法,或者使用帶有事務的數據庫來實現嚴格意義上的分佈式鎖。
參考資料
Distributed locks with Redis
基於Redis的分佈式鎖到底安全嗎(上)? - 鐵蕾的我的博客
https://martin.kleppmann.com/...