新聞中心
前言
創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),沿河企業(yè)網(wǎng)站建設(shè),沿河品牌網(wǎng)站建設(shè),網(wǎng)站定制,沿河網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,沿河網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。近兩年來(lái)微服務(wù)變得越來(lái)越熱門(mén),越來(lái)越多的應(yīng)用部署在分布式環(huán)境中,在分布式環(huán)境中,數(shù)據(jù)一致性是一直以來(lái)需要關(guān)注并且去解決的問(wèn)題,分布式鎖也就成為了一種廣泛使用的技術(shù),常用的分布式實(shí)現(xiàn)方式為Redis,Zookeeper,其中基于Redis的分布式鎖的使用更加廣泛。
但是在工作和網(wǎng)絡(luò)上看到過(guò)各個(gè)版本的Redis分布式鎖實(shí)現(xiàn),每種實(shí)現(xiàn)都有一些不嚴(yán)謹(jǐn)?shù)牡胤?,甚至有可能是錯(cuò)誤的實(shí)現(xiàn),包括在代碼中,如果不能正確的使用分布式鎖,可能造成嚴(yán)重的生產(chǎn)環(huán)境故障,本文主要對(duì)目前遇到的各種分布式鎖以及其缺陷做了一個(gè)整理,并對(duì)如何選擇合適的Redis分布式鎖給出建議。
各個(gè)版本的Redis分布式鎖
V1.0
tryLock(){?? ????SETNX?Key?1 ????EXPIRE?Key?Seconds } release(){?? ??DELETE?Key }
這個(gè)版本應(yīng)該是最簡(jiǎn)單的版本,也是出現(xiàn)頻率很高的一個(gè)版本,首先給鎖加一個(gè)過(guò)期時(shí)間操作是為了避免應(yīng)用在服務(wù)重啟或者異常導(dǎo)致鎖無(wú)法釋放后,不會(huì)出現(xiàn)鎖一直無(wú)法被釋放的情況。
這個(gè)方案的一個(gè)問(wèn)題在于每次提交一個(gè)Redis請(qǐng)求,如果執(zhí)行完第一條命令后應(yīng)用異?;蛘咧貑?,鎖將無(wú)法過(guò)期,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條命令),但是如果Redis僅執(zhí)行了一條命令后crash或者發(fā)生主從切換,依然會(huì)出現(xiàn)鎖沒(méi)有過(guò)期時(shí)間,最終導(dǎo)致無(wú)法釋放。
另外一個(gè)問(wèn)題在于,很多同學(xué)在釋放分布式鎖的過(guò)程中,無(wú)論鎖是否獲取成功,都在finally中釋放鎖,這樣是一個(gè)鎖的錯(cuò)誤使用,這個(gè)問(wèn)題將在后續(xù)的V3.0版本中解決。
針對(duì)鎖無(wú)法釋放問(wèn)題的一個(gè)解決方案基于GETSET命令來(lái)實(shí)現(xiàn)
V1.1 基于GETSET
tryLock(){?? ????NewExpireTime=CurrentTimestamp+ExpireSeconds ????if(SETNX?Key?NewExpireTime?Seconds){ ?????????oldExpireTime?=?GET(Key) ??????????if(?oldExpireTime?思路:
(1)SETNX(Key,ExpireTime)獲取鎖
(2)如果獲取鎖失敗,通過(guò)GET(Key)返回的時(shí)間戳檢查鎖是否已經(jīng)過(guò)期
(3)GETSET(Key,ExpireTime)修改Value為NewExpireTime
(4)檢查GETSET返回的舊值,如果等于GET返回的值,則認(rèn)為獲取鎖成功
注意:這個(gè)版本去掉了EXPIRE命令,改為通過(guò)Value時(shí)間戳值來(lái)判斷過(guò)期
問(wèn)題:
(1)在鎖競(jìng)爭(zhēng)較高的情況下,會(huì)出現(xiàn)Value不斷被覆蓋,但是沒(méi)有一個(gè)Client獲取到鎖
(2)在獲取鎖的過(guò)程中不斷的修改原有鎖的數(shù)據(jù),設(shè)想一種場(chǎng)景C1,C2競(jìng)爭(zhēng)鎖,C1獲取到了鎖,C2鎖執(zhí)行了GETSET操作修改了C1鎖的過(guò)期時(shí)間,如果C1沒(méi)有正確釋放鎖,鎖的過(guò)期時(shí)間被延長(zhǎng),其它Client需要等待更久的時(shí)間
V2.0 基于SETNX
tryLock(){?? ????SETNX?Key?1?Seconds } release(){?? ??DELETE?Key }Redis 2.6.12版本后SETNX增加過(guò)期時(shí)間參數(shù),這樣就解決了兩條命令無(wú)法保證原子性的問(wèn)題。但是設(shè)想下面一個(gè)場(chǎng)景:
(1)C1成功獲取到了鎖,之后C1因?yàn)镚C進(jìn)入等待或者未知原因?qū)е氯蝿?wù)執(zhí)行過(guò)長(zhǎng),最后在鎖失效前C1沒(méi)有主動(dòng)釋放鎖
(2)C2在C1的鎖超時(shí)后獲取到鎖,并且開(kāi)始執(zhí)行,這個(gè)時(shí)候C1和C2都同時(shí)在執(zhí)行,會(huì)因重復(fù)執(zhí)行造成數(shù)據(jù)不一致等未知情況
(3)C1如果先執(zhí)行完畢,則會(huì)釋放C2的鎖,此時(shí)可能導(dǎo)致另外一個(gè)C3進(jìn)程獲取到了鎖
大致的流程圖
存在問(wèn)題:(1)由于C1的停頓導(dǎo)致C1 和C2同都獲得了鎖并且同時(shí)在執(zhí)行,在業(yè)務(wù)實(shí)現(xiàn)間接要求必須保證冪等性
(2)C1釋放了不屬于C1的鎖
V3.0
tryLock(){?? ????SETNX?Key?UnixTimestamp?Seconds } release(){?? ????EVAL( ??????//LuaScript ??????if?redis.call("get",KEYS[1])?==?ARGV[1]?then ??????????return?redis.call("del",KEYS[1]) ??????else ??????????return?0 ??????end ????) }這個(gè)方案通過(guò)指定Value為時(shí)間戳,并在釋放鎖的時(shí)候檢查鎖的Value是否為獲取鎖的Value,避免了V2.0版本中提到的C1釋放了C2持有的鎖的問(wèn)題;另外在釋放鎖的時(shí)候因?yàn)樯婕暗蕉鄠€(gè)Redis操作,并且考慮到Check And Set 模型的并發(fā)問(wèn)題,所以使用Lua腳本來(lái)避免并發(fā)問(wèn)題。
存在問(wèn)題:
如果在并發(fā)極高的場(chǎng)景下,比如搶紅包場(chǎng)景,可能存在UnixTimestamp重復(fù)問(wèn)題,另外由于不能保證分布式環(huán)境下的物理時(shí)鐘一致性,也可能存在UnixTimestamp重復(fù)問(wèn)題,只不過(guò)極少情況下會(huì)遇到。
V3.1
tryLock(){?? ????SET?Key?UniqId?Seconds } release(){?? ????EVAL( ??????//LuaScript ??????if?redis.call("get",KEYS[1])?==?ARGV[1]?then ??????????return?redis.call("del",KEYS[1]) ??????else ??????????return?0 ??????end ????) }Redis 2.6.12后SET同樣提供了一個(gè)NX參數(shù),等同于SETNX命令,官方文檔上提醒后面的版本有可能去掉SETNX, SETEX, PSETEX,并用SET命令代替,另外一個(gè)優(yōu)化是使用一個(gè)自增的唯一UniqId代替時(shí)間戳來(lái)規(guī)避V3.0提到的時(shí)鐘問(wèn)題。
這個(gè)方案是目前最優(yōu)的分布式鎖方案,但是如果在Redis集群環(huán)境下依然存在問(wèn)題:
由于Redis集群數(shù)據(jù)同步為異步,假設(shè)在Master節(jié)點(diǎn)獲取到鎖后未完成數(shù)據(jù)同步情況下Master節(jié)點(diǎn)crash,此時(shí)在新的Master節(jié)點(diǎn)依然可以獲取鎖,所以多個(gè)Client同時(shí)獲取到了鎖
分布式Redis鎖:Redlock
V3.1的版本僅在單實(shí)例的場(chǎng)景下是安全的,針對(duì)如何實(shí)現(xiàn)分布式Redis的鎖,國(guó)外的分布式專家有過(guò)激烈的討論, antirez提出了分布式鎖算法Redlock,在distlock話題下可以看到對(duì)Redlock的詳細(xì)說(shuō)明,下面是Redlock算法的一個(gè)中文說(shuō)明(引用)
假設(shè)有N個(gè)獨(dú)立的Redis節(jié)點(diǎn)
1、獲取當(dāng)前時(shí)間(毫秒數(shù))。
2、按順序依次向N個(gè)Redis節(jié)點(diǎn)執(zhí)行獲取鎖的操作。這個(gè)獲取操作跟前面基于單Redis節(jié)點(diǎn)的獲取鎖的過(guò)程相同,包含隨機(jī)字符串my_random_value,也包含過(guò)期時(shí)間(比如PX 30000,即鎖的有效時(shí)間)。
為了保證在某個(gè)Redis節(jié)點(diǎn)不可用的時(shí)候算法能夠繼續(xù)運(yùn)行,這個(gè)獲取鎖的操作還有一個(gè)超時(shí)時(shí)間(time out),它要遠(yuǎn)小于鎖的有效時(shí)間(幾十毫秒量級(jí))??蛻舳嗽谙蚰硞€(gè)Redis節(jié)點(diǎn)獲取鎖失敗以后,應(yīng)該立即嘗試下一個(gè)Redis節(jié)點(diǎn)。
這里的失敗,應(yīng)該包含任何類型的失敗,比如該Redis節(jié)點(diǎn)不可用,或者該Redis節(jié)點(diǎn)上的鎖已經(jīng)被其它客戶端持有(注:Redlock原文中這里只提到了Redis節(jié)點(diǎn)不可用的情況,但也應(yīng)該包含其它的失敗情況)。
3、計(jì)算整個(gè)獲取鎖的過(guò)程總共消耗了多長(zhǎng)時(shí)間,計(jì)算方法是用當(dāng)前時(shí)間減去第1步記錄的時(shí)間。如果客戶端從大多數(shù)Redis節(jié)點(diǎn)(>= N/2+1)成功獲取到了鎖,并且獲取鎖總共消耗的時(shí)間沒(méi)有超過(guò)鎖的有效時(shí)間(lock validity time),那么這時(shí)客戶端才認(rèn)為最終獲取鎖成功;否則,認(rèn)為最終獲取鎖失敗。
4、如果最終獲取鎖成功了,那么這個(gè)鎖的有效時(shí)間應(yīng)該重新計(jì)算,它等于最初的鎖的有效時(shí)間減去第3步計(jì)算出來(lái)的獲取鎖消耗的時(shí)間。
5、如果最終獲取鎖失敗了(可能由于獲取到鎖的Redis節(jié)點(diǎn)個(gè)數(shù)少于N/2+1,或者整個(gè)獲取鎖的過(guò)程消耗的時(shí)間超過(guò)了鎖的最初有效時(shí)間),那么客戶端應(yīng)該立即向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作(即前面介紹的Redis Lua腳本)。
6、釋放鎖:對(duì)所有的Redis節(jié)點(diǎn)發(fā)起釋放鎖操作
然而Martin Kleppmann針對(duì)這個(gè)算法提出了質(zhì)疑,提出應(yīng)該基于fencing token機(jī)制(每次對(duì)資源進(jìn)行操作都需要進(jìn)行token驗(yàn)證)
(1)Redlock在系統(tǒng)模型上尤其是在分布式時(shí)鐘一致性問(wèn)題上提出了假設(shè),實(shí)際場(chǎng)景下存在時(shí)鐘不一致和時(shí)鐘跳躍問(wèn)題,而Redlock恰恰是基于timing的分布式鎖
(2)另外Redlock由于是基于自動(dòng)過(guò)期機(jī)制,依然沒(méi)有解決長(zhǎng)時(shí)間的gc pause等問(wèn)題帶來(lái)的鎖自動(dòng)失效,從而帶來(lái)的安全性問(wèn)題。
接著antirez又回復(fù)了Martin Kleppmann的質(zhì)疑,給出了過(guò)期機(jī)制的合理性,以及實(shí)際場(chǎng)景中如果出現(xiàn)停頓問(wèn)題導(dǎo)致多個(gè)Client同時(shí)訪問(wèn)資源的情況下如何處理。
針對(duì)Redlock的問(wèn)題,基于Redis的分布式鎖到底安全嗎給出了詳細(xì)的中文說(shuō)明,并對(duì)Redlock算法存在的問(wèn)題提出了分析。歡迎大家關(guān)注我的公種浩【程序員追風(fēng)】,整理了1000道2019年多家公司java面試題400多頁(yè)pdf文檔,文章都會(huì)在里面更新,整理的資料也會(huì)放在里面。
總結(jié)
不論是基于SETNX版本的Redis單實(shí)例分布式鎖,還是Redlock分布式鎖,都是為了保證以下特性
(1)安全性:在同一時(shí)間不允許多個(gè)Client同時(shí)持有鎖
(2)活性 死鎖:鎖最終應(yīng)該能夠被釋放,即使Client端crash或者出現(xiàn)網(wǎng)絡(luò)分區(qū)(通常基于超時(shí)機(jī)制) 容錯(cuò)性:只要超過(guò)半數(shù)Redis節(jié)點(diǎn)可用,鎖都能被正確獲取和釋放
所以在開(kāi)發(fā)或者使用分布式鎖的過(guò)程中要保證安全性和活性,避免出現(xiàn)不可預(yù)測(cè)的結(jié)果。
另外每個(gè)版本的分布式鎖都存在一些問(wèn)題,在鎖的使用上要針對(duì)鎖的實(shí)用場(chǎng)景選擇合適的鎖,通常情況下鎖的使用場(chǎng)景包括:
(1)Efficiency(效率):只需要一個(gè)Client來(lái)完成操作,不需要重復(fù)執(zhí)行,這是一個(gè)對(duì)寬松的分布式鎖,只需要保證鎖的活性即可;
(2)Correctness(正確性):多個(gè)Client保證嚴(yán)格的互斥性,不允許出現(xiàn)同時(shí)持有鎖或者對(duì)同時(shí)操作同一資源,這種場(chǎng)景下需要在鎖的選擇和使用上更加嚴(yán)格,同時(shí)在業(yè)務(wù)代碼上盡量做到冪等
在Redis分布式鎖的實(shí)現(xiàn)上還有很多問(wèn)題等待解決,我們需要認(rèn)識(shí)到這些問(wèn)題并清楚如何正確實(shí)現(xiàn)一個(gè)Redis 分布式鎖,然后在工作中合理的選擇和正確的使用分布式鎖。
Redisson分布式鎖實(shí)現(xiàn)原理的總結(jié)了一張圖
最后
歡迎大家一起交流,喜歡文章記得關(guān)注我點(diǎn)贊轉(zhuǎn)發(fā)喲,感謝支持!
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無(wú)理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國(guó)服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡(jiǎn)單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。
標(biāo)題名稱:一篇文章帶你解讀Redis分布式鎖的發(fā)展史和正確實(shí)現(xiàn)方式-創(chuàng)新互聯(lián)
分享路徑:http://www.dlmjj.cn/article/ceggdc.html