Redis鎖在面試中是Redis繞不開的話題,關于Redis鎖神帅,網(wǎng)上很多文章再姑,大多都是這個方案:
1、單機Redis
2找御、RedLock
3元镀、Redission分布式鎖
本文基于這三個點,延伸出幾個問題霎桅,同時介紹下Martin和RedLock實現(xiàn)作者Salvatore的論點栖疑。
當然分布式鎖并不只限于這兩種,還有基于ZooKeeper的分布式鎖的實現(xiàn)滔驶、Chubby的分布式鎖遇革、Mysql分布式鎖、基于 Etcd、Hazelcast?分布式鎖的等等萝快,其中關于基于ZooKeeper的分布式鎖的內(nèi)容將放在下一次進行講解锻霎。
一 單機Redis
單節(jié)點的分布式鎖,網(wǎng)上已經(jīng)有很多介紹了揪漩,很容易找到這樣一句代碼:
SET key value NX PX30000
如果上面的指令成功執(zhí)行旋恼,那也就意味著獲得了鎖,此時可以對共享資源進行操作奄容,如果指令執(zhí)行失敗冰更,則意味著獲取鎖失敗。
雖然這個指令非常簡單昂勒,其中的幾個問題還是值得思考的
1 ? px有必要嗎蜀细。為什么?
首先想如果沒有PX叁怪,會發(fā)生什么情況审葬,假設一個進程A獲得了這把鎖,但是這個進程掛了奕谭,此時無法釋放這把鎖涣觉,那是不是這個進程就會一直持有這把鎖呢?這樣就導致其他進程無法獲得共享資源血柳。
所以需要這個PX對鎖設置一個過期時間官册,防止其他進程無法獲得鎖。
思考另外一個問題难捌,由于我們的進程運行在機器上膝宁,而機器運行的狀態(tài)我們無法確定。進程A此時獲得了鎖根吁,此時可能因為GC停頓導致了程序的卡頓员淫,亦或是調(diào)用其他接口因為擁塞網(wǎng)絡問題導致請求變慢,也許其他進程發(fā)送SIGSTOP信號击敌,總之此時因為一些其他情況造成進程暫停介返,又有一個進程B獲得了鎖,如果此時進程A的過期時間到達沃斤,使用DEL刪除這個記錄圣蝎,那么就會把進程B所拿到的鎖進行刪除,這樣就會產(chǎn)生一個不安全的事故衡瓶,所以PX的設置還是有必要的徘公。
(超時后只有對key執(zhí)行DEL命令或者SET命令或者GETSET時才會清除)
進程A的Stop-The-World GC時間結束,發(fā)現(xiàn)鎖過期哮针,反手DEL刪除了鎖关面,此時也就是刪除了進程B所持有的鎖坦袍。
那么如何解決這個問題呢,Martin給了一個解決方案缭裆,采用一個單調(diào)遞增的數(shù)字來確定延遲到來的請求键闺,其實也就相當于版本號,如果接收到低版本的請求澈驼,便拒絕就好了辛燥。
但是這樣還是會有一個問題:如果因為兩個客戶端都發(fā)生了GC,但是版本號到達的順序是正確的缝其,那么是不是又有問題了挎塌?
2 ??過期時間設置多少合適,超時之后内边,共享資源是不是失去保護了呢榴都?
Redis鎖的過期時間設置,也是可以思考的點漠其,例如Redis鎖過期了嘴高,但是業(yè)務邏輯卻沒有執(zhí)行完成,那該怎么辦呢和屎?
如果把Redis鎖的過期時間再設置長一點怎么樣拴驮?這樣還是有問題,如果此時業(yè)務邏輯執(zhí)行的非巢裥牛快套啤,過長的Redis鎖過期時間的設置反而會降低效率。
如果此時客戶端因為一些原因?qū)е骆i過期了随常,那么這里的客戶端訪問接下來的共享資源還安全嗎潜沦?答案是否定的,共享資源此時已經(jīng)失去了保護绪氛。
3 ??隨機字符串有必要嗎唆鸡,為什么?
首先隨機字符串是必要的枣察,這保證了客戶端釋放所必須是自己所持有的鎖喇闸。我們可以假設一個場景,如果這個字符串是一個固定不變的字符串询件,那么還是會產(chǎn)生類似問題一的結果:A所屬的鎖被B釋放。因此這個隨機字符串還是有必要的唆樊。
4 ??指令可以拆成兩條指令實現(xiàn)嗎,缺點有什么?
加鎖的指令確實可以通過兩條指令進行實現(xiàn)割择,例如:
SETNXkey valueEXPIRE key 30
這樣做弊端很明顯藕各,就是沒有原子操作舆瘪,另外還有一個需要說的點,對于SETNX指令红伦,在Redis官方文檔上SET指令介紹有這樣一句話:
大意是SET指令可以替換SETNX英古、SETEX、PSETEX昙读,因此在Redis的將來版本中召调,這些命令可能會被棄用并最終刪除。
5蛮浑、釋放鎖的方式怎么做唠叛?
釋放鎖的步驟其實包含三部分:獲取、判斷和刪除沮稚,而這三個步驟如果想要一次執(zhí)行艺沼,那必須要保證原子性,可以想想為什么要保證刪除鎖的步驟是原子性蕴掏?
如果這三個步驟不是原子性障般,那么和第一個問題一樣,將會發(fā)生下列執(zhí)行情況:
進程A刪除了屬于進程B的同一個共享資源的鎖:
由于Redis 服務器會單線程原子性執(zhí)行 lua 腳本盛杰,保證 lua 腳本在處理的過程中不會被任意其它請求打斷挽荡,這便是刪除鎖的Lua腳本。
if redis.call('get', KEYS[1]) == ARGV[1] ????then?? ? ? ? return redis.call('del', KEYS[1]) ????else?? ? ? ? return 0 end
二 主從部署Redis
思考另外一個問題饶唤,Redis如果是單機部署徐伐,有什么問題呢?
首先單點部署自然有其不穩(wěn)定性募狂,主從部署可以提高其可用性办素,但是就算擁有了failover故障轉(zhuǎn)移機制,對于單節(jié)點Redis鎖還是一樣有問題祸穷,這也是單節(jié)點Redis的分布式鎖所無法解決的問題性穿。
主從部署有什么問題呢
假設Redis此時是單節(jié)點部署,這個節(jié)點突然宕機了雷滚,那么其他鏈接這個服務的客戶端都沒有辦法獲得這個鎖需曾,因此為了提高單節(jié)點的可用性,可以給這個單節(jié)點部署一個Slave節(jié)點祈远,這樣一旦當主節(jié)點不可用的時候呆万,通過failover自動轉(zhuǎn)移機制,服務切換到Slave節(jié)點上车份。
但是我們也知道谋减,Redis服務切換這個過程是異步復制的,如果此時存儲Key的Master節(jié)點宕機扫沼,而存儲這個鎖的Key沒有同步到Slave節(jié)點節(jié)點上出爹,此時Slave因為failover自動轉(zhuǎn)移機制升級到Master節(jié)點庄吼,則其他客戶端一樣可以獲得同一個共享資源的鎖。
因為主從復制集群不能保證安全屬性(即不能保證一個時刻严就,只有一個客戶端獲得鎖)总寻,所以這便是的Redis主從集群的問題所在。
三 RedLock
基于Redis單節(jié)點以及主從部署的Redis分布式鎖存在的問題梢为,因此催生了RedLock的實現(xiàn)渐行,那么RedLock又是什么呢。
3.1?RedLock介紹
在Redis的集群模式下抖誉,假設我們有奇數(shù)個Redis節(jié)點(這里假設有五個)殊轴,我們部署在不同的機器上,保證這幾臺機器不會同時的宕機袒炉,獲取鎖和釋放鎖的時候都由客戶端去通知這里的五個節(jié)點旁理,最終結果交給客戶端,由客戶端決定是否加鎖成功我磁。
3.2?RedLock獲取鎖
那么此時獲取鎖的步驟就是:
1孽文、獲取當前的系統(tǒng)時間,以毫秒為單位夺艰。
2芋哭、依次向N個Redis實例節(jié)點執(zhí)行獲取鎖的操作,使用和單節(jié)點的獲取鎖操作一樣郁副,使用相同的key和隨機值獲取鎖减牺。
? ? ? 向單個節(jié)點獲取鎖的時候,應該設置一個超時時間和一個網(wǎng)絡鏈接存谎,這個時間小于鎖的失效時間拔疚。(避免Redis掛了,客戶端還在等鎖的結果既荚,如果超時時間沒有獲取到稚失,應該立刻獲取下一個Redis實例)
3、計算獲取鎖的時間花費了多少(使用當前時間減去第一步的系統(tǒng)時間)恰聘。獲取鎖成功的條件是句各,客戶端從大于一半的Redis節(jié)點獲取到鎖的時候,并且使用的時間小于鎖的有效時間(lock validity time)這樣才是獲取成功晴叨,否則是失敗凿宾。
4、重新計算獲取鎖的有效時間:等于最初的有效時間減去第三部計算出來獲得鎖的時間兼蕊。
5菌湃、如果最終獲取失敗,客戶端在所有Redis節(jié)點發(fā)起釋放鎖的操作(包括沒有加鎖成功的節(jié)點遍略,使用Lua腳本解鎖)
這里我們也可以看到一個基礎問題惧所,那就是RedLock鎖很依賴每個Redis節(jié)點的時間信息,當然在Redis官網(wǎng)里面也寫著:
算法基于這樣一個假設:雖然多個進程之間沒有時鐘同步绪杏,但每個進程都以相同的時鐘頻率前進下愈,時間差相對于失效時間來說幾乎可以忽略不計。這種假設和我們的真實世界非常接近:每個計算機都有一個本地時鐘蕾久,我們可以容忍多個計算機之間有較小的時鐘漂移势似。
3.3?RedLock釋放鎖的步驟
刪除鎖的操作就是上面第五步,客戶端向所有Redis節(jié)點發(fā)起釋放鎖的操作(即使這些Redis實例沒有加鎖成功)
這里有一個疑問:為什么要向所有Redis實例發(fā)送釋放鎖的過程僧著,只需要發(fā)送已經(jīng)加鎖的實例不就好了履因?
想象一個這樣的情況,我們的加鎖請求在一個Redis實例上已經(jīng)加鎖成功了盹愚,但是由于Redis實例和客戶端是網(wǎng)絡傳輸栅迄,此時響應包在網(wǎng)絡中傳輸丟失,那么在客戶端看來就是失敗了皆怕,但是在Redis集群內(nèi)部則認為這個節(jié)點加鎖成功毅舆。因此我們必須要向所有Redis節(jié)點發(fā)送釋放鎖的操作。
3.4 客戶端的阻塞導致鎖過期
在上面的基于單節(jié)點Redis實現(xiàn)的分布式鎖導致過期的情況愈腾,有一個這樣的情況憋活,如果是因為客戶端的問題導致鎖過期,此時共享資源就已經(jīng)不安全了虱黄,那么在RedLock里面是否有所改進呢悦即?
答案是沒有,因為在獲得鎖的過程中的第四步橱乱,重新計算鎖的有效時間辜梳,此時因為客戶端阻塞導致鎖過期,而鎖的有效時間又過短仅醇,那么共享資源還是不安全的了冗美。
3.5?RedLock爭論的點
對于RedLock的實現(xiàn),也是存在很多開發(fā)者和研究人員對其的爭論析二,爭論的便是RedLock是否是安全的粉洼。
這里我們先假設一種情況,如果此時有五個Redis節(jié)點叶摄,客戶端A和客戶端B獲取同一個共享資源:
1属韧、客戶端A鎖住了1、2蛤吓、3號Redis節(jié)點宵喂,超過一半獲得鎖成功。
2会傲、此時1號節(jié)點因為崩潰重新啟動锅棕,由于RDB或者AOF的恢復拙泽,在1號節(jié)點上加鎖信息沒有恢復過來。
3裸燎、客戶端B鎖住了1顾瞻、4、5號節(jié)點德绿,獲取鎖成功荷荤。
那么此時客戶端A和客戶端B都可以對共享資源進行操作。
當然Redis的作者antirez也考慮到了這一種情況移稳,antirez采用了延遲加載來去解決這個問題:
延遲重啟也就是一個Redis節(jié)點掛了蕴纳,先不重啟,等到大于鎖的有效時間的時候才重啟這個節(jié)點个粱,這樣就不會影響現(xiàn)有鎖了古毛。
3.6 Martin和Salvatore的爭論
有名的事件便是Martin和Salvatore的爭論,這個爭論的事情又是怎么樣產(chǎn)生的呢几蜻?
在這個Redis官網(wǎng)里面有一段話喇潘,如果你使用的是分布式系統(tǒng),你的觀點和意見很重要梭稚。
Martin Kleppmann是一個專門研究分布式的開發(fā)人員颖低,正好研究了RedLock,于是Martin Kleppmann于2016年2月8日發(fā)布的一篇文章"How to do distributed locking"弧烤,分析了RedLock在鎖的安全上存在一些問題
“neither fish nor fowl”非驢非馬
這便是Martin對于RedLock算法的評價忱屑。
2016年2月9日Redlock的原始作者 Salvatore對本文發(fā)表了反駁"Is Redlock safe?"來反駁Martin Kleppmann的論點。據(jù)Martin說他在公開那篇文章的之前一周暇昂,已經(jīng)發(fā)送給Salvatore進行查看和討論莺戒,但是公開發(fā)表的第二天Salvatore就發(fā)送了這篇反駁的文章。
隨后這場爭論也引發(fā)了許多人的討論急波,而這其中的內(nèi)容从铲,不得不說真的是高手過招,有目共賞澄暮,值得反復觀看名段。
Martin文章
Martin Kleppmann的文章前半部分涉及了很多分布式基礎性的問題,對于一個程序員來說泣懊,非常值得一看伸辟。
首先Martin認為分布式鎖的目的主要有兩個:效率和正確性
效率:當我們使用分布式鎖的場景是效率,也就是說為了避免兩次操作的重復性時馍刮,我們完全不必要使用Redis的RedLock的方式信夫,首先RedLock的成本并不低,多個Redis實例的搭建只是為了獲得鎖去減少重復操作,未免顯得有些浪費静稻,此時可以使用單實例Redis來進行減少重復操作的目的警没。
正確性:保證分布式并發(fā)程序的執(zhí)行順序性是分布式鎖的另一個核心目的。
Martin的前半部分敘述內(nèi)容就相當于和之前第一部分所講的內(nèi)容重復姊扔,這里便不再敘述了惠奸。
第二部分內(nèi)容是針對于讓RedLock失效的討論。Martin認為RedLock強依賴系統(tǒng)時鐘恰梢,一旦這個時鐘不準確,這個算法的安全性也就無法保證梗掰。換句話說分布式的情況多種多樣嵌言,對于一個好的分布式算法來說,安全性應該是很重要的及穗,它不應該因為其他因素而導致系統(tǒng)的安全性丟失摧茴,最多是不能給出結果而已(這里可以看下Paxos協(xié)議)。
Salvatore反駁
Salvatore對其的反駁也是非常有意思埂陆,他認為通過恰當?shù)倪\維就可以避免始終發(fā)生大的跳動苛白,但是關于客戶端長時間的暫停的行為,RedLock已經(jīng)在設計的時候避免了焚虱。
Salvatore認為如果客戶端發(fā)生暫停购裙,那么在RedLock獲得鎖的時候,消耗了很長時間鹃栽,不會讓客戶端拿到一個它認為有效躏率,但是實際上已經(jīng)無效的鎖(當然基于系統(tǒng)時鐘沒有發(fā)生跳躍的前提)。
回憶起RedLock加鎖的過程:
1民鼓、獲取當前時間
2薇芝、向Redis集群實例發(fā)起請求獲取鎖
3、再獲取當前時間
4丰嘉、兩時間相減夯到,查看獲取鎖的時間,查看我們是否足夠快獲得鎖
5饮亏、客戶端拿到鎖做一些工作
Note steps 1 and 3. Whatever delay happens in the network or in the processes involved, after acquiring the majority we *check again* that we are not out of time.?
Salvatore認為這在第四步的時候就避免了客戶端花費時間過長導致的鎖過期耍贾,因為都會再次檢查。
The delay can only happen after steps 3, resulting into the lock to be considered ok while actually expired, that is, we are back at the first problem Martin identified of distributed locks where the client fails to stop working to the shared resource before the lock validity expires.?
但是延遲至發(fā)生在第三步之后克滴,這將會導致鎖被認為是有效的逼争,而實際已經(jīng)過期了,也就是Martin指出的第一個問題上劝赔,客戶端沒能夠在鎖的有效期過期之前完成對共享資源的操作
Let me tell again how this problem is common with *all the distributed locks implementations*, and how the token as a solution is both unrealistic and can be used with Redlock as well.
讓我再說一下誓焦,這個問題對于所有的分布式鎖的實現(xiàn)是普遍存在的,而且基于token的這種解決方案是不切實際的,又可以和Redlock一起用杂伟。
其實antirez也認同時鐘跳躍會發(fā)生延遲移层,導致鎖的不安全性,但是適當運維不讓時鐘發(fā)生跳躍的大前提下赫粥,我RedLock算法是安全的观话,在設計的時候就考慮了這些,如果你說第三步之后發(fā)生的問題越平,那是其他分布式鎖普遍存在的频蛔。
當然這些爭論只開頭,后面還有很多大牛參與進去秦叛,具體的內(nèi)容在這里晦溪,有興趣的小伙伴可以繼續(xù)探討學習:
四 Redission分布式鎖
之前所說對于鎖的過期時間的設置,在單機情況下的設置總有其不方便之處挣跋,那么如果有一個實現(xiàn)三圆,可以對Redis鎖的過期時間進行修改,到了過期時間進行自動續(xù)期就好了避咆,那就會方便很多舟肉,不會去根據(jù)業(yè)務場景去設置這個過期時間。
而基于Redission實現(xiàn)的分布式鎖便解決了這個問題查库,Redssion是在加鎖成功之后路媚,注冊一個定時任務去監(jiān)聽這個鎖,每隔一段時間去檢查這個鎖膨报,如果還持有這個鎖磷籍,那就修改過期時間,對這個過期時間進行續(xù)期现柠。這個延長過期時間的機制也叫做watchdog“看門狗”機制院领。
加鎖的過程實際上也是調(diào)用了一個Lua腳本,這里只是畫圖表明了這個Lua腳本加鎖的過程够吩,有興趣可以查看下Redisson加鎖的Lua腳本源碼比然。
釋放鎖的過程其實最后也是調(diào)用了一個Lua腳本,這里也是畫出了Lua腳本釋放的過程周循。
五 總結
最后我這邊還是比較認同Martin的看法强法,借助他針對于分布式鎖使用場景的結論,給出一個總結:
如果為了效率而使用分布式鎖湾笛,并且允許鎖的偶然失效饮怯,那么使用簡單實現(xiàn)且效率高的單節(jié)點Redis鎖。
如果為了正確性保證絕對不失效的場景下嚎研,那么不要使用RedLock的實現(xiàn)蓖墅,盡量采用支持事務的數(shù)據(jù)庫亦或是Zookeeper實現(xiàn)的分布式鎖。
當然對于分布式的情況多種多樣,客戶端和服務器之間的延遲论矾,會對所有分布式鎖的實現(xiàn)都會帶來影響教翩,這也是不可避免的,也是值得我們思考的贪壳。
另外Martin在這個爭論的最后也發(fā)表了一個評論饱亿,在這里也分享給大家:
?I don’t care who is right or wrong in this debate — I care about learning from others’ work, so that we can avoid repeating old mistakes, and make things better in future. So much great work has already been done for us: by standing on the shoulders of giants, we can build better software.
在這場辯論中,我不在乎誰對誰錯闰靴,我在乎的是從別人的工作中學到東西彪笼,這樣我們就可以避免重蹈覆轍,并且使未來變得更好蚂且。這么多偉大的工作前人已經(jīng)為我們做了:站在巨人的肩膀上杰扫,我們可以建立更好的軟件。
其實到這里還是有些意猶未盡膘掰,腦子里也會有很多爭論的聲音,不過學習似乎就是這樣佳遣,有很多問題得多問問自己為什么识埋,探討其中的原理,便能學到更多零渐。
如果覺得不錯窒舟,歡迎關注公眾號: Bee風