基于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)狸捅,還有很多細節(jié),需要仔細斟酌累提,因為在分布式并發(fā)多進程中尘喝,任何一點出現(xiàn)差錯,都會導(dǎo)致死鎖斋陪,hold住所有進程朽褪。

加鎖實現(xiàn)

SETNX 可以直接加鎖操作,比如說對某個關(guān)鍵詞foo加鎖鳍贾,客戶端可以嘗試
SETNX foo.lock <current unix time>

如果返回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中的時間戳進行對比塞赂,如果超過一定差值泪勒,認為鎖已經(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 <current unix time>
并得到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ā)下被壓垮诽里。最為常見的問題還有

在此我向大家推薦一個架構(gòu)學(xué)習(xí)交流群袒餐。交流學(xué)習(xí)群號:736220120 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis谤狡,Netty源碼分析灸眼,高并發(fā)、高性能墓懂、分布式焰宣、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化捕仔、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系匕积。還能領(lǐng)取免費的學(xué)習(xí)資源,目前受益良多榜跌。

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)嗜诀,就會引起死鎖,很悲催孔祸,我在做的時候就碰到了隆敢,具體怎么碰到的看下面的問題

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)兩者不同夹界,處理邏輯認為未獲取鎖馆里。
C2 向foo.lock發(fā)送GETSET命令,獲取返回值T22(C1寫入的時間戳)可柿。
C2 比對C2和C22發(fā)現(xiàn)兩者不同鸠踪,處理邏輯認為未獲取鎖。

此時C1和C2都認為未獲取鎖复斥,其實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倍涧偷。
在這種情況下,我們壓測的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í)行壓力情況

上圖中細高部分是為未采用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閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凶伙,死亡現(xiàn)場離奇詭異郭毕,居然都是意外死亡,警方通過查閱死者的電腦和手機函荣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門显押,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人傻挂,你說我怎么就攤上這事乘碑。” “怎么了金拒?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵兽肤,是天一觀的道長。 經(jīng)常有香客問我殖蚕,道長轿衔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任睦疫,我火速辦了婚禮害驹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蛤育。我一直安慰自己宛官,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布瓦糕。 她就那樣靜靜地躺著底洗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咕娄。 梳的紋絲不亂的頭發(fā)上亥揖,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音,去河邊找鬼费变。 笑死摧扇,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挚歧。 我是一名探鬼主播扛稽,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼滑负!你這毒婦竟也來了在张?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤矮慕,失蹤者是張志新(化名)和其女友劉穎帮匾,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凡傅,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡辟狈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了夏跷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哼转。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖槽华,靈堂內(nèi)的尸體忽然破棺而出壹蔓,到底是詐尸還是另有隱情,我是刑警寧澤猫态,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布佣蓉,位于F島的核電站,受9級特大地震影響亲雪,放射性物質(zhì)發(fā)生泄漏勇凭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一义辕、第九天 我趴在偏房一處隱蔽的房頂上張望虾标。 院中可真熱鬧,春花似錦灌砖、人聲如沸璧函。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蘸吓。三九已至,卻和暖如春撩幽,著一層夾襖步出監(jiān)牢的瞬間库继,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宪萄,地道東北人舅桩。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像雨膨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子读串,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

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