Redis-分布式鎖

分布式鎖的由來

現(xiàn)在的業(yè)務(wù)應(yīng)用通常都是微服務(wù)架構(gòu)十偶,這也意味著一個(gè)應(yīng)用會(huì)部署多個(gè)進(jìn)程蚂会,那這多個(gè)進(jìn)程如果需要修改 MySQL 中的同一行記錄時(shí)转捕,為了避免操作亂序?qū)е聰?shù)據(jù)錯(cuò)誤作岖,此時(shí),我們就需要引入「分布式鎖」來解決這個(gè)問題了五芝。想要實(shí)現(xiàn)分布式鎖痘儡,必須借助一個(gè)外部系統(tǒng),所有進(jìn)程都去這個(gè)系統(tǒng)上申請「加鎖」枢步。而這個(gè)外部系統(tǒng)沉删,必須要實(shí)現(xiàn)「互斥」的能力,即兩個(gè)請求同時(shí)進(jìn)來醉途,只會(huì)給一個(gè)進(jìn)程返回成功矾瑰,另一個(gè)返回失敗(或等待)隘擎。這個(gè)外部系統(tǒng)殴穴,可以是 MySQL,也可以是 Redis 或 Zookeeper货葬。但為了追求更好的性能采幌,我們通常會(huì)選擇使用 Redis 或 Zookeeper 來做。

如何實(shí)現(xiàn)分布式鎖

想要實(shí)現(xiàn)分布式鎖震桶,必須要求 Redis 有「互斥」的能力休傍,我們可以使用 SETNX 命令,這個(gè)命令表示SET if Not eXists蹲姐,即如果 key 不存在磨取,才會(huì)設(shè)置它的值,否則什么也不做淤堵。兩個(gè)客戶端進(jìn)程可以執(zhí)行這個(gè)命令寝衫,達(dá)到互斥,就可以實(shí)現(xiàn)一個(gè)分布式鎖拐邪。
客戶端 1 申請加鎖慰毅,加鎖成功:
127.0.0.1:6379> SETNX lock 1
(integer) 1     // 客戶端1,加鎖成功

客戶端 2 申請加鎖扎阶,因?yàn)樗蟮竭_(dá)汹胃,加鎖失敗:
127.0.0.1:6379> SETNX lock 1
(integer) 0     // 客戶端2东臀,加鎖失敗
此時(shí)着饥,加鎖成功的客戶端,就可以去操作「共享資源」惰赋,例如宰掉,修改 MySQL 的某一行數(shù)據(jù)呵哨,或者調(diào)用一個(gè) API 請求。操作完成后轨奄,還要及時(shí)釋放鎖孟害,給后來者讓出操作共享資源的機(jī)會(huì)。如何釋放鎖呢挪拟?也很簡單挨务,直接使用 DEL 命令刪除這個(gè) key 即可:
127.0.0.1:6379> DEL lock // 釋放鎖
(integer) 1

但是,它存在一個(gè)很大的問題玉组,當(dāng)客戶端 1 拿到鎖后谎柄,如果發(fā)生下面的場景,就會(huì)造成「死鎖」:
- 程序處理業(yè)務(wù)邏輯異常惯雳,沒及時(shí)釋放鎖
- 進(jìn)程掛了朝巫,沒機(jī)會(huì)釋放鎖

避免死鎖的方案

 在申請鎖時(shí),給這把鎖設(shè)置一個(gè)「租期」吨凑。在 Redis 中實(shí)現(xiàn)時(shí)就是給這個(gè) key 設(shè)置一個(gè)「過期時(shí)間」捍歪。這里我們假設(shè)操作共享資源的時(shí)間不會(huì)超過 10s户辱,那么在加鎖時(shí)鸵钝,給這個(gè) key 設(shè)置 10s 過期即可:
127.0.0.1:6379> SETNX lock 1    // 加鎖
(integer) 1
127.0.0.1:6379> EXPIRE lock 10  // 10s后自動(dòng)過期
(integer) 1
這樣一來,無論客戶端是否異常庐镐,這個(gè)鎖都可以在 10s 后被「自動(dòng)釋放」恩商,其它客戶端依舊可以拿到鎖。如果這樣操作的話加鎖必逆、設(shè)置過期是 2 條命令怠堪,有沒有可能只執(zhí)行了第一條,第二條卻「來不及」執(zhí)行的情況發(fā)生呢名眉?例如:
- SETNX 執(zhí)行成功粟矿,執(zhí)行 EXPIRE 時(shí)由于網(wǎng)絡(luò)問題執(zhí)行失敗
- SETNX 執(zhí)行成功,Redis 異常宕機(jī)损拢,EXPIRE 沒有機(jī)會(huì)執(zhí)行
- SETNX 執(zhí)行成功陌粹,客戶端異常崩潰,EXPIRE 也沒有機(jī)會(huì)執(zhí)行
總之福压,這兩條命令不能保證是原子操作(一起成功)掏秩,就有潛在的風(fēng)險(xiǎn)導(dǎo)致過期時(shí)間設(shè)置失敗,依舊發(fā)生「死鎖」問題荆姆。

在 Redis 2.6.12 版本之前蒙幻,我們需要想盡辦法,保證 SETNX 和 EXPIRE 原子性執(zhí)行胆筒,還要考慮各種異常情況如何處理邮破。但在 Redis 2.6.12 之后,Redis 擴(kuò)展了 SET 命令的參數(shù),用這一條命令就可以了:

// 一條命令保證原子性執(zhí)行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK

這樣就解決了死鎖問題抒和,也比較簡單队询。
試想這樣一種場景:
客戶端 1 加鎖成功開始操作共享資源,客戶端 1 操作共享資源的時(shí)間构诚,「超過」了鎖的過期時(shí)間蚌斩,鎖被「自動(dòng)釋放」
客戶端 2 加鎖成功,開始操作共享資源
客戶端 1 操作共享資源完成范嘱,釋放鎖(但釋放的是客戶端 2 的鎖)
這里存在兩個(gè)嚴(yán)重的問題:
- 鎖過期:客戶端 1 操作共享資源耗時(shí)太久送膳,導(dǎo)致鎖被自動(dòng)釋放,之后被客戶端 2 持有
- 釋放別人的鎖:客戶端 1 操作共享資源完成后丑蛤,卻又釋放了客戶端 2 的鎖

第一個(gè)問題叠聋,可能是我們評估操作共享資源的時(shí)間不準(zhǔn)確導(dǎo)致的。
例如受裹,操作共享資源的時(shí)間「最慢」可能需要 15s碌补,而我們卻只設(shè)置了 10s 過期,那這就存在鎖提前過期的風(fēng)險(xiǎn)棉饶。過期時(shí)間太短厦章,那增大冗余時(shí)間,例如設(shè)置過期時(shí)間為 20s照藻,這樣總可以了吧袜啃?這樣確實(shí)可以「緩解」這個(gè)問題,降低出問題的概率幸缕,但依舊無法「徹底解決」問題群发。原因在于,客戶端在拿到鎖之后发乔,在操作共享資源時(shí)熟妓,遇到的場景有可能是很復(fù)雜的,例如栏尚,程序內(nèi)部發(fā)生異常起愈、網(wǎng)絡(luò)請求超時(shí)等等。既然是「預(yù)估」時(shí)間抵栈,也只能是大致計(jì)算告材,除非你能預(yù)料并覆蓋到所有導(dǎo)致耗時(shí)變長的場景,但這其實(shí)很難古劲。

第二個(gè)問題在于斥赋,一個(gè)客戶端釋放了其它客戶端持有的鎖。
重點(diǎn)在于产艾,每個(gè)客戶端在釋放鎖時(shí)疤剑,都是「無腦」操作滑绒,并沒有檢查這把鎖是否還「歸自己持有」,所以就會(huì)發(fā)生釋放別人鎖的風(fēng)險(xiǎn)隘膘,這樣的解鎖流程疑故,很不「嚴(yán)謹(jǐn)」!

如何解決鎖的唯一性

解決辦法是:客戶端在加鎖時(shí)弯菊,設(shè)置一個(gè)只有自己知道的「唯一標(biāo)識(shí)」進(jìn)去纵势。例如,可以是自己的線程 ID管钳,也可以是一個(gè) UUID(隨機(jī)且唯一)钦铁。
// 鎖的VALUE設(shè)置為UUID
// 假設(shè) 20s 操作共享時(shí)間完全足夠,先不考慮鎖自動(dòng)過期的問題才漆。
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK

在釋放鎖時(shí)牛曹,要先判斷這把鎖是否還歸自己持有再釋放鎖操作。
if redis.get("lock") == $uuid:
    redis.del("lock")

這里釋放鎖使用的是 GET + DEL 兩條命令醇滥,這時(shí)又會(huì)遇到我們前面講的原子性問題了黎比。
- 客戶端 1 執(zhí)行 GET,判斷鎖是自己的
- 客戶端 2 執(zhí)行了 SET 命令鸳玩,強(qiáng)制獲取到鎖(雖然發(fā)生概率比較低阅虫,但我們需要嚴(yán)謹(jǐn)?shù)乜紤]鎖的安全性模型)
- 客戶端 1 執(zhí)行 DEL,卻釋放了客戶端 2 的鎖
由此可見怀喉,這兩個(gè)命令還是必須要原子執(zhí)行才行书妻。
我們可以把這個(gè)邏輯船响,寫成 Lua 腳本躬拢,讓 Redis 來執(zhí)行。因?yàn)?Redis 處理每一個(gè)請求是「單線程」執(zhí)行的见间,在執(zhí)行一個(gè) Lua 腳本時(shí)聊闯,其它請求必須等待,直到這個(gè) Lua 腳本處理完成米诉,這樣一來菱蔬,GET + DEL 之間就不會(huì)插入其它命令了。

// 判斷鎖是自己的史侣,才釋放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

基于 Redis 實(shí)現(xiàn)的分布式鎖拴泌,一個(gè)嚴(yán)謹(jǐn)?shù)牡牧鞒倘缦拢?br> - 加鎖:SET lock_key unique_id EXexpire_time NX
- 操作共享資源
- 釋放鎖:Lua 腳本,先 GET 判斷鎖是否歸屬自己惊橱,再 DEL 釋放鎖

如何評估鎖過期的時(shí)間

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚪腐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子税朴,更是在濱河造成了極大的恐慌回季,老刑警劉巖家制,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泡一,居然都是意外死亡颤殴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門鼻忠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涵但,“玉大人,你說我怎么就攤上這事帖蔓∠桶剩” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵讨阻,是天一觀的道長芥永。 經(jīng)常有香客問我,道長钝吮,這世上最難降的妖魔是什么埋涧? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮奇瘦,結(jié)果婚禮上棘催,老公的妹妹穿的比我還像新娘。我一直安慰自己耳标,他們只是感情好醇坝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著次坡,像睡著了一般呼猪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砸琅,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天宋距,我揣著相機(jī)與錄音,去河邊找鬼症脂。 笑死谚赎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诱篷。 我是一名探鬼主播壶唤,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼棕所!你這毒婦竟也來了闸盔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤橙凳,失蹤者是張志新(化名)和其女友劉穎蕾殴,沒想到半個(gè)月后笑撞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钓觉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年茴肥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荡灾。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓤狐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出批幌,到底是詐尸還是另有隱情础锐,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布荧缘,位于F島的核電站皆警,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏截粗。R本人自食惡果不足惜信姓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绸罗。 院中可真熱鬧意推,春花似錦、人聲如沸珊蟀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽育灸。三九已至腻窒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間描扯,已是汗流浹背定页。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绽诚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓杭煎,卻偏偏與公主長得像恩够,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子羡铲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

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