百度做網(wǎng)站按點(diǎn)擊量收費(fèi)嗎品牌廣告圖片
分布式鎖概念
在多線程的程序里,為了避免同時(shí)操作一個(gè)共享變量產(chǎn)生數(shù)據(jù)問(wèn)題,會(huì)加一個(gè)互斥鎖,以確保共享變量的正確性,使用范圍是同一個(gè)進(jìn)程。
那如果是多個(gè)進(jìn)程,需要同時(shí)操作一個(gè)共享資源,如何互斥呢?
比如,現(xiàn)在的業(yè)務(wù)基本上都是微服務(wù)架構(gòu),一個(gè)應(yīng)用會(huì)部署多個(gè)進(jìn)程,這多個(gè)進(jìn)程需要修改MySQL的同一行記錄時(shí),就需要引入分布式鎖來(lái)解決這個(gè)問(wèn)題了。
要實(shí)現(xiàn)分布式鎖,需要借助一個(gè)外部系統(tǒng),所有的進(jìn)程都去這個(gè)系統(tǒng)上申請(qǐng)加鎖,而這個(gè)外部系統(tǒng)必須要實(shí)現(xiàn)互斥的能力,換言之,如果有兩個(gè)請(qǐng)求同時(shí)進(jìn)來(lái),也只會(huì)給一個(gè)進(jìn)程返回成功,另一個(gè)返回失敗或等待。
這個(gè)外部系統(tǒng)可以是MySQL、Redis或Zookeeper,一般使用Redis或ZK
如何實(shí)現(xiàn)
可以使用SETNX
命令,表示SET IF NOT EXISTS
,也就是當(dāng)Key不存在的時(shí)候才會(huì)設(shè)置他的值。
比如,客戶端1申請(qǐng)加鎖,加鎖成功;
127.0.0.1:6379> SETNX lock 1
(integer) 1
客戶端2申請(qǐng)加鎖,因?yàn)榈竭_(dá)的比客戶端1晚,加鎖失敗。
127.0.0.2:6379> SETNX lock 1
(integer) 0
此時(shí),加鎖成功的客戶端,就可以去操作共享資源,例如,修改MySQL的某一行數(shù)據(jù),或是調(diào)用一個(gè)API請(qǐng)求。
操作完成后,還要釋放鎖,這樣后續(xù)的客戶端才能繼續(xù)操作共享資源。
釋放鎖可以通過(guò)DEL
命令刪除這個(gè)key
以上是分布式鎖最簡(jiǎn)單的實(shí)現(xiàn),存在一個(gè)很大的問(wèn)題,如果客戶端1拿到鎖以后,沒(méi)有釋放鎖,就會(huì)造成死鎖。沒(méi)有釋放鎖的原因有以下幾個(gè):
-
程序處理業(yè)務(wù)邏輯異常,沒(méi)有及時(shí)釋放鎖
-
整個(gè)進(jìn)程掛了/宕機(jī),沒(méi)有辦法去釋放鎖
如何避免死鎖
那么如何解決上述的問(wèn)題呢?比較容易想到的方案是:申請(qǐng)鎖的時(shí)候,給鎖設(shè)置一個(gè)租期
對(duì)于剛剛的Redis實(shí)現(xiàn),就是給這個(gè)Key設(shè)置一個(gè)過(guò)期時(shí)間
比如
127.0.0.1:6379> SETNX lock 1 // 加鎖
(integer) 1
127.0.0.1:6379> EXPIRE lock 10 // 10s后自動(dòng)過(guò)期
(integer) 1
這樣如果客戶端異常的話,這個(gè)鎖在10秒后會(huì)被自動(dòng)釋放,其他客戶端依舊可以拿到鎖
這樣還是有問(wèn)題,剛剛的操作里,加鎖、設(shè)置過(guò)期時(shí)間這是2條命令,有可能執(zhí)行完第一條,第二條來(lái)不及執(zhí)行,比如:
-
執(zhí)行第二條語(yǔ)句時(shí)因?yàn)榫W(wǎng)絡(luò)問(wèn)題,執(zhí)行失敗
-
Redis異常宕機(jī),第二條沒(méi)時(shí)間執(zhí)行
-
客戶端異常崩潰/退出,第二條命令沒(méi)機(jī)會(huì)執(zhí)行
如果這兩條命令不能保證原子操作,就有潛在的風(fēng)險(xiǎn)導(dǎo)致過(guò)期時(shí)間設(shè)置失敗,依舊會(huì)發(fā)生死鎖。
Redis 2.6.12以后,只需要使用 SET lock 1 EX 10 NX
就可以了
接下來(lái)分析下還有沒(méi)有別的問(wèn)題?
假設(shè)這樣一種場(chǎng)景:
-
客戶端1加鎖成功,開(kāi)始操作共享資源
-
客戶端1操作共享資源的時(shí)間,超過(guò)了鎖的過(guò)期時(shí)間,鎖被自動(dòng)釋放
-
客戶端2加鎖成功,開(kāi)始操作共享資源
-
客戶端1操作共享資源完成,釋放鎖(注意這里的鎖是客戶端2剛剛加的鎖)
這里有兩個(gè)很?chē)?yán)重的問(wèn)題:
-
鎖過(guò)期:客戶端1操作共享資源耗時(shí)太久,導(dǎo)致鎖被自動(dòng)釋放,之后客戶端2加鎖
-
釋放別人的鎖:客戶端1操作共享資源后,釋放了客戶端2的鎖
第一個(gè)問(wèn)題,可能是我們?cè)u(píng)估共享資源的時(shí)間不準(zhǔn)確導(dǎo)致的。
簡(jiǎn)單的解決方案就是增大冗余時(shí)間,比如10秒過(guò)期,但是操作共享資源的時(shí)間最慢是15秒,那我就設(shè)置過(guò)期時(shí)間為20秒。但是這樣沒(méi)法根本解決問(wèn)題,預(yù)估的時(shí)間只是大致計(jì)算,并不能預(yù)估到所有增加耗時(shí)的場(chǎng)景,比如程序內(nèi)部發(fā)生異常、網(wǎng)絡(luò)請(qǐng)求超時(shí)、異常耗時(shí)增加等。
第二個(gè)問(wèn)題,一個(gè)客戶端釋放了其他客戶端持有的鎖
導(dǎo)致這個(gè)問(wèn)題的原因是 每個(gè)客戶端在釋放鎖的時(shí)候,沒(méi)有檢查這個(gè)鎖是不是自己加上的
如何解決鎖被別人釋放的問(wèn)題
客戶端在加鎖的時(shí)候,設(shè)置一個(gè)只有自己知道的唯一標(biāo)識(shí)進(jìn)去,可以是自己的主機(jī)名字、線程ID等,也可以是一個(gè)UUID
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK
之后釋放鎖的時(shí)候,需要檢查以下
if redis.get(“l(fā)ock”) == $uuid:
????redis.del(“l(fā)ock”)
這里又是一個(gè)原子操作,可以寫(xiě)成lua
腳本,讓Redis來(lái)執(zhí)行
if redis.call("GET",KEYS[1]) == ARGV[1]
thenreturn redis.call("DEL",KEYS[1])
elsereturn 0
end
這樣,整個(gè)加鎖和解鎖的過(guò)程就比較嚴(yán)謹(jǐn)了,大概流程如下:
-
加鎖:
SET lock $uuid EX 10 NX
-
操作共享資源
-
釋放鎖:
lua
腳本
不好評(píng)估鎖過(guò)期時(shí)間怎么辦
前面還有一個(gè)遺留問(wèn)題就是,鎖會(huì)有提前過(guò)期的風(fēng)險(xiǎn)
簡(jiǎn)單的方案就是盡量冗余過(guò)期時(shí)間,降低鎖提前過(guò)期的概率。但是這個(gè)方案并不能完美解決問(wèn)題。
比較主流的方案是,加鎖的時(shí)候,先設(shè)置一個(gè)過(guò)期時(shí)間,同時(shí)開(kāi)啟一個(gè)守護(hù)線程,定時(shí)去檢測(cè)這個(gè)鎖的失效時(shí)間,如果鎖快過(guò)期了,但是操作共享資源還未完成,自動(dòng)對(duì)鎖進(jìn)行續(xù)期,重新設(shè)置過(guò)期時(shí)間。
在Java
里Redisson
庫(kù)已經(jīng)把這部分封裝好了,看門(mén)狗線程
分布式場(chǎng)景
之前分析的場(chǎng)景都是,鎖在單個(gè)Redis實(shí)例中可能會(huì)產(chǎn)生的問(wèn)題,并沒(méi)有考慮Redis的部署架構(gòu)細(xì)節(jié)
但實(shí)際上在使用Redis的時(shí)候,都是采用主從集群+哨兵的模式部署,當(dāng)主庫(kù)異常宕機(jī)的時(shí)候,哨兵可以進(jìn)行故障自動(dòng)切換,把從庫(kù)提升為主庫(kù),繼續(xù)提供服務(wù),來(lái)保證可用性。
假設(shè)這樣一個(gè)場(chǎng)景:
-
客戶端1在主庫(kù)上加鎖成功
-
主庫(kù)宕機(jī),SET命令還未同步到從庫(kù)上(這里是因?yàn)?strong>主從同步是異步的)
-
哨兵把從庫(kù)提升為主庫(kù),這個(gè)鎖在新的主庫(kù)上就丟失了
當(dāng)引入Redis副本后,分布式鎖還是可能受影響,為此,Redis的作者提出一種解決方案,就是Redlock 紅鎖