基于Redis實現(xiàn)分布式鎖

背景

在很多互聯(lián)網(wǎng)產(chǎn)品應(yīng)用中刻蟹,有些場景需要加鎖處理,比如:秒殺觉渴,全局遞增ID介劫,樓層生成等等徽惋。大部分的解決方案是基于DB實現(xiàn)的案淋,Redis為單進程單線程模式,采用隊列模式將并發(fā)訪問變成串行訪問险绘,且多客戶端對Redis的連接并不存在競爭關(guān)系踢京。其次Redis提供一些命令SETNX誉碴,GETSET,可以方便實現(xiàn)分布式鎖機制瓣距。

Redis命令介紹

使用Redis實現(xiàn)分布式鎖黔帕,有兩個重要函數(shù)需要介紹

SETNX命令(SET if Not eXists)

語法:

SETNX key value

功能:

當(dāng)且僅當(dāng) key 不存在,將 key 的值設(shè)為 value 蹈丸,并返回1成黄;若給定的 key 已經(jīng)存在,則 SETNX 不做任何動作逻杖,并返回0奋岁。

GETSET命令

語法:

GETSET key value

功能:

將給定 key 的值設(shè)為 value ,并返回 key 的舊值 (old value)荸百,當(dāng) key 存在但不是字符串類型時闻伶,返回一個錯誤,當(dāng)key不存在時够话,返回nil蓝翰。

GET命令

語法:

GET key

功能:

返回 key 所關(guān)聯(lián)的字符串值,如果 key 不存在那么返回特殊值 nil 女嘲。

DEL命令

語法:

DEL key [KEY …]

功能:

刪除給定的一個或多個 key ,不存在的 key 會被忽略畜份。

兵貴精,不在多欣尼。分布式鎖漂坏,我們就依靠這四個命令。但在具體實現(xiàn)媒至,還有很多細(xì)節(jié)顶别,需要仔細(xì)斟酌,因為在分布式并發(fā)多進程中拒啰,任何一點出現(xiàn)差錯驯绎,都會導(dǎo)致死鎖,hold住所有進程谋旦。

加鎖實現(xiàn)

SETNX 可以直接加鎖操作剩失,比如說對某個關(guān)鍵詞foo加鎖,客戶端可以嘗試

SETNX foo.lock

如果返回1册着,表示客戶端已經(jīng)獲取鎖拴孤,可以往下操作,操作完成后甲捏,通過

DEL foo.lock

命令來釋放鎖演熟。

如果返回0,說明foo已經(jīng)被其他客戶端上鎖,如果鎖是非堵塞的芒粹,可以選擇返回調(diào)用兄纺。如果是堵塞調(diào)用調(diào)用,就需要進入以下個重試循環(huán)化漆,直至成功獲得鎖或者重試超時估脆。理想是美好的,現(xiàn)實是殘酷的。僅僅使用SETNX加鎖帶有競爭條件的,在某些特定的情況會造成死鎖錯誤枫浙。

處理死鎖

在上面的處理方式中,如果獲取鎖的客戶端端執(zhí)行時間過長棺聊,進程被kill掉,或者因為其他異常崩潰贞谓,導(dǎo)致無法釋放鎖限佩,就會造成死鎖。所以裸弦,需要對加鎖要做時效性檢測祟同。因此,我們在加鎖時理疙,把當(dāng)前時間戳作為value存入此鎖中晕城,通過當(dāng)前時間戳和Redis中的時間戳進行對比,如果超過一定差值窖贤,認(rèn)為鎖已經(jīng)時效砖顷,防止鎖無限期的鎖下去,但是赃梧,在大并發(fā)情況滤蝠,如果同時檢測鎖失效,并簡單粗暴的刪除死鎖授嘀,再通過SETNX上鎖物咳,可能會導(dǎo)致競爭條件的產(chǎn)生,即多個客戶端同時獲取鎖蹄皱。

C1獲取鎖览闰,并崩潰。C2和C3調(diào)用SETNX上鎖返回0后巷折,獲得foo.lock的時間戳压鉴,通過比對時間戳,發(fā)現(xiàn)鎖超時锻拘。

C2 向foo.lock發(fā)送DEL命令油吭。

C2 向foo.lock發(fā)送SETNX獲取鎖。

C3 向foo.lock發(fā)送DEL命令,此時C3發(fā)送DEL時上鞠,其實DEL掉的是C2的鎖际邻。

C3 向foo.lock發(fā)送SETNX獲取鎖芯丧。

此時C2和C3都獲取了鎖芍阎,產(chǎn)生競爭條件,如果在更高并發(fā)的情況缨恒,可能會有更多客戶端獲取鎖谴咸。所以,DEL鎖的操作骗露,不能直接使用在鎖超時的情況下岭佳,幸好我們有GETSET方法,假設(shè)我們現(xiàn)在有另外一個客戶端C4萧锉,看看如何使用GETSET方式珊随,避免這種情況產(chǎn)生。

C1獲取鎖柿隙,并崩潰叶洞。C2和C3調(diào)用SETNX上鎖返回0后,調(diào)用GET命令獲得foo.lock的時間戳T1禀崖,通過比對時間戳衩辟,發(fā)現(xiàn)鎖超時。

C4 向foo.lock發(fā)送GESET命令波附,

GETSET foo.lock

并得到foo.lock中老的時間戳T2

如果T1=T2艺晴,說明C4獲得時間戳。

如果T1!=T2掸屡,說明C4之前有另外一個客戶端C5通過調(diào)用GETSET方式獲取了時間戳封寞,C4未獲得鎖。只能sleep下仅财,進入下次循環(huán)中钥星。

現(xiàn)在唯一的問題是,C4設(shè)置foo.lock的新時間戳满着,是否會對鎖產(chǎn)生影響谦炒。其實我們可以看到C4和C5執(zhí)行的時間差值極小,并且寫入foo.lock中的都是有效時間錯风喇,所以對鎖并沒有影響宁改。

為了讓這個鎖更加強壯,獲取鎖的客戶端魂莫,應(yīng)該在調(diào)用關(guān)鍵業(yè)務(wù)時还蹲,再次調(diào)用GET方法獲取T1,和寫入的T0時間戳進行對比,以免鎖因其他情況被執(zhí)行DEL意外解開而不知谜喊。以上步驟和情況潭兽,很容易從其他參考資料中看到《范簦客戶端處理和失敗的情況非常復(fù)雜山卦,不僅僅是崩潰這么簡單,還可能是客戶端因為某些操作被阻塞了相當(dāng)長時間诵次,緊接著 DEL 命令被嘗試執(zhí)行(但這時鎖卻在另外的客戶端手上)账蓉。也可能因為處理不當(dāng),導(dǎo)致死鎖逾一。還有可能因為sleep設(shè)置不合理铸本,導(dǎo)致Redis在大并發(fā)下被壓垮。最為常見的問題還有

GET返回nil時應(yīng)該走那種邏輯遵堵?

第一種走超時邏輯

C1客戶端獲取鎖箱玷,并且處理完后,DEL掉鎖陌宿,在DEL鎖之前锡足。C2通過SETNX向foo.lock設(shè)置時間戳T0 發(fā)現(xiàn)有客戶端獲取鎖,進入GET操作限番。

C2 向foo.lock發(fā)送GET命令舱污,獲取返回值T1(nil)。

C2 通過T0>T1+expire對比弥虐,進入GETSET流程扩灯。

C2 調(diào)用GETSET向foo.lock發(fā)送T0時間戳,返回foo.lock的原值T2

C2 如果T2=T1相等霜瘪,獲得鎖珠插,如果T2!=T1,未獲得鎖颖对。

第二種情況走循環(huán)走setnx邏輯

C1客戶端獲取鎖捻撑,并且處理完后,DEL掉鎖缤底,在DEL鎖之前顾患。C2通過SETNX向foo.lock設(shè)置時間戳T0 發(fā)現(xiàn)有客戶端獲取鎖,進入GET操作个唧。

C2 向foo.lock發(fā)送GET命令江解,獲取返回值T1(nil)。

C2 循環(huán)徙歼,進入下一次SETNX邏輯

兩種邏輯貌似都是OK犁河,但是從邏輯處理上來說鳖枕,第一種情況存在問題。當(dāng)GET返回nil表示桨螺,鎖是被刪除的宾符,而不是超時,應(yīng)該走SETNX邏輯加鎖灭翔。走第一種情況的問題是魏烫,正常的加鎖邏輯應(yīng)該走SETNX,而現(xiàn)在當(dāng)鎖被解除后缠局,走的是GETST则奥,如果判斷條件不當(dāng)考润,就會引起死鎖狭园,很悲催,我在做的時候就碰到了糊治,具體怎么碰到的看下面的問題

?Java架構(gòu)交流學(xué)習(xí)圈:681065582 面向具有Java開發(fā)經(jīng)驗人群 幫助突破瓶頸 提升思維能力

GETSET返回nil時應(yīng)該怎么處理唱矛?

C1和C2客戶端調(diào)用GET接口,C1返回T1井辜,此時C3網(wǎng)絡(luò)情況更好绎谦,快速進入獲取鎖,并執(zhí)行DEL刪除鎖粥脚,C2返回T2(nil)窃肠,C1和C2都進入超時處理邏輯。

C1 向foo.lock發(fā)送GETSET命令刷允,獲取返回值T11(nil)冤留。

C1 比對C1和C11發(fā)現(xiàn)兩者不同,處理邏輯認(rèn)為未獲取鎖树灶。

C2 向foo.lock發(fā)送GETSET命令纤怒,獲取返回值T22(C1寫入的時間戳)。

C2 比對C2和C22發(fā)現(xiàn)兩者不同天通,處理邏輯認(rèn)為未獲取鎖泊窘。

此時C1和C2都認(rèn)為未獲取鎖,其實C1是已經(jīng)獲取鎖了像寒,但是他的處理邏輯沒有考慮GETSET返回nil的情況烘豹,只是單純的用GET和GETSET值就行對比,至于為什么會出現(xiàn)這種情況诺祸?一種是多客戶端時携悯,每個客戶端連接Redis的后,發(fā)出的命令并不是連續(xù)的序臂,導(dǎo)致從單客戶端看到的好像連續(xù)的命令蚌卤,到Redis server后实束,這兩條命令之間可能已經(jīng)插入大量的其他客戶端發(fā)出的命令,比如DEL,SETNX等逊彭。第二種情況咸灿,多客戶端之間時間不同步,或者不是嚴(yán)格意義的同步侮叮。

時間戳的問題

我們看到foo.lock的value值為時間戳避矢,所以要在多客戶端情況下,保證鎖有效囊榜,一定要同步各服務(wù)器的時間审胸,如果各服務(wù)器間,時間有差異卸勺。時間不一致的客戶端砂沛,在判斷鎖超時,就會出現(xiàn)偏差曙求,從而產(chǎn)生競爭條件碍庵。

鎖的超時與否,嚴(yán)格依賴時間戳悟狱,時間戳本身也是有精度限制静浴,假如我們的時間精度為秒,從加鎖到執(zhí)行操作再到解鎖挤渐,一般操作肯定都能在一秒內(nèi)完成苹享。這樣的話,我們上面的CASE浴麻,就很容易出現(xiàn)得问。所以,最好把時間精度提升到毫秒級白胀。這樣的話椭赋,可以保證毫秒級別的鎖是安全的。

分布式鎖的問題

1:必要的超時機制:獲取鎖的客戶端一旦崩潰或杠,一定要有過期機制哪怔,否則其他客戶端都降無法獲取鎖,造成死鎖問題向抢。

2:分布式鎖认境,多客戶端的時間戳不能保證嚴(yán)格意義的一致性,所以在某些特定因素下挟鸠,有可能存在鎖串的情況叉信。要適度的機制,可以承受小概率的事件產(chǎn)生艘希。

3:只對關(guān)鍵處理節(jié)點加鎖硼身,良好的習(xí)慣是硅急,把相關(guān)的資源準(zhǔn)備好,比如連接數(shù)據(jù)庫后佳遂,調(diào)用加鎖機制獲取鎖营袜,直接進行操作,然后釋放丑罪,盡量減少持有鎖的時間荚板。

4:在持有鎖期間要不要CHECK鎖,如果需要嚴(yán)格依賴鎖的狀態(tài)吩屹,最好在關(guān)鍵步驟中做鎖的CHECK檢查機制跪另,但是根據(jù)我們的測試發(fā)現(xiàn),在大并發(fā)時煤搜,每一次CHECK鎖操作免绿,都要消耗掉幾個毫秒,而我們的整個持鎖處理邏輯才不到10毫秒宅楞,玩客沒有選擇做鎖的檢查针姿。

5:sleep學(xué)問袱吆,為了減少對Redis的壓力厌衙,獲取鎖嘗試時,循環(huán)之間一定要做sleep操作绞绒。但是sleep時間是多少是門學(xué)問婶希。需要根據(jù)自己的Redis的QPS,加上持鎖處理時間等進行合理計算蓬衡。

6:至于為什么不使用Redis的muti喻杈,expire,watch等機制狰晚,可以查一參考資料筒饰,找下原因。

鎖測試數(shù)據(jù)

未使用sleep

第一種壁晒,鎖重試時未做sleep瓷们。單次請求,加鎖秒咐,執(zhí)行谬晕,解鎖時間

可以看到加鎖和解鎖時間都很快,當(dāng)我們使用

ab -n1000 -c100 'http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t'

AB 并發(fā)100累計1000次請求携取,對這個方法進行壓測時攒钳。

我們會發(fā)現(xiàn),獲取鎖的時間變成雷滋,同時持有鎖后不撑,執(zhí)行時間也變成文兢,而delete鎖的時間,將近10ms時間焕檬,為什么會這樣禽作?

1:持有鎖后,我們的執(zhí)行邏輯中包含了再次調(diào)用Redis操作揩页,在大并發(fā)情況下旷偿,Redis執(zhí)行明顯變慢。

2:鎖的刪除時間變長爆侣,從之前的0.2ms萍程,變成9.8ms,性能下降近50倍兔仰。

3:Java架構(gòu)交流學(xué)習(xí)圈:681065582 面向具有Java開發(fā)經(jīng)驗人群 幫助突破瓶頸 提升思維能力

在這種情況下茫负,我們壓測的QPS為49,最終發(fā)現(xiàn)QPS和壓測總量有關(guān)乎赴,當(dāng)我們并發(fā)100總共100次請求時忍法,QPS得到110多。當(dāng)我們使用sleep時

使用Sleep時

單次執(zhí)行請求時

我們看到榕吼,和不使用sleep機制時饿序,性能相當(dāng)。當(dāng)時用相同的壓測條件進行壓縮時

獲取鎖的時間明顯變長羹蚣,而鎖的釋放時間明顯變短原探,僅是不采用sleep機制的一半。當(dāng)然執(zhí)行時間變成就是因為顽素,我們在執(zhí)行過程中咽弦,重新創(chuàng)建數(shù)據(jù)庫連接,導(dǎo)致時間變長的胁出。同時我們可以對比下Redis的命令執(zhí)行壓力情況

上圖中細(xì)高部分是為未采用sleep機制的時的壓測圖型型,矮胖部分為采用sleep機制的壓測圖,通上圖看到壓力減少50%左右全蝶,當(dāng)然闹蒜,sleep這種方式還有個缺點QPS下降明顯,在我們的壓測條件下裸诽,僅為35嫂用,并且有部分請求出現(xiàn)超時情況。不過綜合各種情況后丈冬,我們還是決定采用sleep機制嘱函,主要是為了防止在大并發(fā)情況下把Redis壓垮,很不行埂蕊,我們之前碰到過往弓,所以肯定會采用sleep機制疏唾。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市函似,隨后出現(xiàn)的幾起案子槐脏,更是在濱河造成了極大的恐慌,老刑警劉巖撇寞,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顿天,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔑担,警方通過查閱死者的電腦和手機牌废,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啤握,“玉大人鸟缕,你說我怎么就攤上這事∨盘В” “怎么了懂从?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蹲蒲。 經(jīng)常有香客問我番甩,道長,這世上最難降的妖魔是什么悠鞍? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任对室,我火速辦了婚禮,結(jié)果婚禮上咖祭,老公的妹妹穿的比我還像新娘。我一直安慰自己蔫骂,他們只是感情好么翰,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辽旋,像睡著了一般浩嫌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上补胚,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天码耐,我揣著相機與錄音,去河邊找鬼溶其。 笑死骚腥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瓶逃。 我是一名探鬼主播束铭,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼廓块,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了契沫?” 一聲冷哼從身側(cè)響起带猴,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懈万,沒想到半個月后拴清,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡会通,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年贷掖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渴语。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡苹威,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驾凶,到底是詐尸還是另有隱情牙甫,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布调违,位于F島的核電站窟哺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏技肩。R本人自食惡果不足惜且轨,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虚婿。 院中可真熱鬧旋奢,春花似錦、人聲如沸然痊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剧浸。三九已至锹引,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唆香,已是汗流浹背嫌变。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躬它,地道東北人腾啥。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碑宴。 傳聞我的和親對象是個殘疾皇子软啼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容