分布式鎖的各種實(shí)現(xiàn),看完這篇你就懂了!
前言
今天我們講講分布式鎖,網(wǎng)上相關(guān)的內(nèi)容有很多,但是比較分散,剛好自己剛學(xué)習(xí)完總結(jié)下,分享給大家,文章內(nèi)容會(huì)比較多,我們先從思維導(dǎo)圖中了解要講的內(nèi)容。
圖片
什么是分布式鎖
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式,通過互斥來保持一致性。
了解分布式鎖之前先了解下線程鎖和進(jìn)程鎖:
線程鎖:主要用來給方法、代碼塊加鎖。當(dāng)某個(gè)方法或代碼使用鎖,在同一時(shí)刻僅有一個(gè)線程執(zhí)行該方法或該代碼段。線程鎖只在同一JVM中有效果,因?yàn)榫€程鎖的實(shí)現(xiàn)在根本上是依靠線程之間共享內(nèi)存實(shí)現(xiàn)的,比如Synchronized、Lock等
進(jìn)程鎖:控制同一操作系統(tǒng)中多個(gè)進(jìn)程訪問某個(gè)共享資源,因?yàn)檫M(jìn)程具有獨(dú)立性,各個(gè)進(jìn)程無法訪問其他進(jìn)程的資源,因此無法通過synchronized等線程鎖實(shí)現(xiàn)進(jìn)程鎖
比如Golang語言中的sync包就提供了基本的同步基元,如互斥鎖
但是以上兩種適合在單體架構(gòu)應(yīng)用,但是分布式系統(tǒng)中多個(gè)服務(wù)節(jié)點(diǎn),多個(gè)進(jìn)程分散部署在不同節(jié)點(diǎn)機(jī)器中,此時(shí)對(duì)于資源的競(jìng)爭(zhēng),上訴兩種對(duì)節(jié)點(diǎn)本地資源的鎖就無效了。
這個(gè)時(shí)候就需要分布式鎖來對(duì)分布式系統(tǒng)多進(jìn)程訪問資源進(jìn)行控制,因此分布式鎖是為了解決分布式互斥問題!
圖片
分布式鎖的特性
互斥
互斥性很好理解,這也是最基本功能,就是在任意時(shí)刻,只能有一個(gè)客戶端才能獲取鎖,不能同時(shí)有兩個(gè)客戶端獲取到鎖。
避免死鎖
為什么會(huì)出現(xiàn)死鎖,因?yàn)楂@取鎖的客戶端因?yàn)槟承┰?如down機(jī)等)而未能釋放鎖,其它客戶端再也無法獲取到該鎖,從而導(dǎo)致整個(gè)流程無法繼續(xù)進(jìn)行。
圖片
面對(duì)這種情況,當(dāng)然有解決辦法啦!
引入過期時(shí)間:通常情況下我們會(huì)設(shè)置一個(gè) TTL(Time To Live,存活時(shí)間) 來避免死鎖,但是這并不能完全避免。
1. 比如TTL為5秒,進(jìn)程A獲得鎖
2. 問題是5秒內(nèi)進(jìn)程A并未釋放鎖,被系統(tǒng)自動(dòng)釋放,進(jìn)程B獲得鎖
3. 剛好第6秒時(shí)進(jìn)程A執(zhí)行完,又會(huì)釋放鎖,也就是進(jìn)程A釋放了進(jìn)程B的鎖
僅僅加個(gè)過期時(shí)間會(huì)設(shè)計(jì)到兩個(gè)問題:鎖過期和釋放別人的鎖問題
鎖附加唯一性:針對(duì)釋放別人鎖這種問題,我們可以給每個(gè)客戶端進(jìn)程設(shè)置【唯一ID】,這樣我們就可以在應(yīng)用層就進(jìn)行檢查唯一ID。
自動(dòng)續(xù)期:鎖過期問題的出現(xiàn),是我們對(duì)持有鎖的時(shí)間不好進(jìn)行預(yù)估,設(shè)置較短的話會(huì)有【提前過期】風(fēng)險(xiǎn),但是過期時(shí)間設(shè)置過長(zhǎng),可能鎖長(zhǎng)時(shí)間得不到釋放。
這種情況同樣有處理方式,可以開啟一個(gè)守護(hù)進(jìn)程(watch dog),檢測(cè)失效時(shí)間進(jìn)行續(xù)租,比如Java技術(shù)棧可以用Redisson來處理。
可重入:
一個(gè)線程獲取了鎖,但是在執(zhí)行時(shí),又再次嘗試獲取鎖會(huì)發(fā)生什么情況?
是的,導(dǎo)致了重復(fù)獲取鎖,占用了鎖資源,造成了死鎖問題。
我們了解下什么是【可重入】:指的是同一個(gè)線程在持有鎖的情況下,可以多次獲取該鎖而不會(huì)造成死鎖,也就是一個(gè)線程可以在獲取鎖之后再次獲取同一個(gè)鎖,而不需要等待鎖釋放。
解決方式:比如實(shí)現(xiàn)Redis分布式鎖的可重入,在實(shí)現(xiàn)時(shí),需要借助Redis的Lua腳本語言,并使用引用計(jì)數(shù)器技術(shù),保證同一線程可重入鎖的正確性。
容錯(cuò)
容錯(cuò)性是為了當(dāng)部分節(jié)點(diǎn)(redis節(jié)點(diǎn)等)宕機(jī)時(shí),客戶端仍然能夠獲取鎖和釋放鎖,一般來說會(huì)有以下兩種處理方式:
一種像etcd/zookeeper這種作為鎖服務(wù)能夠自動(dòng)進(jìn)行故障切換,因?yàn)樗旧砭褪莻€(gè)集群,另一種可以提供多個(gè)獨(dú)立的鎖服務(wù),客戶端向多個(gè)獨(dú)立鎖服務(wù)進(jìn)行請(qǐng)求,某個(gè)鎖服務(wù)故障時(shí),也可以從其他服務(wù)獲取到鎖信息,但是這種缺點(diǎn)很明顯,客戶端需要去請(qǐng)求多個(gè)鎖服務(wù)。
分類
本文會(huì)講述四種關(guān)于分布式鎖的實(shí)現(xiàn),按實(shí)現(xiàn)方式來看,可以分為兩種:自旋、watch監(jiān)聽
自旋方式
基于數(shù)據(jù)庫(kù)和基于Redis的實(shí)現(xiàn)就是需要在客戶端未獲得鎖時(shí),進(jìn)入一個(gè)循環(huán),不斷的嘗試請(qǐng)求是否能獲得鎖,直到成功或者超時(shí)過期為止。
監(jiān)聽方式
這種方式只需要客戶端Watch監(jiān)聽某個(gè)key就可以了,鎖可用的時(shí)候會(huì)通知客戶端,客戶端不需要反復(fù)請(qǐng)求,基于zooKeeper和基于Etcd實(shí)現(xiàn)分布式鎖就是用這種方式。
實(shí)現(xiàn)方式
分布式鎖的實(shí)現(xiàn)方式有數(shù)據(jù)庫(kù)、基于Redis緩存、ZooKeeper、Etcd等,文章主要從這幾種實(shí)現(xiàn)方式并結(jié)合問題的方式展開敘述!
基于MySQL
利用數(shù)據(jù)庫(kù)表來實(shí)現(xiàn)實(shí)現(xiàn)分布式鎖,是不是感覺有點(diǎn)疑惑,是的,我再寫之前收集資料的時(shí)候也有點(diǎn)疑問,雖然這種方式我們并不推崇,但是我們也可以作為一個(gè)方案來進(jìn)行了解,我們看看到底怎么做的:
比如在數(shù)據(jù)庫(kù)中創(chuàng)建一個(gè)表,表中包含方法名等字段,并在方法名name字段上創(chuàng)建唯一索引,想要執(zhí)行某個(gè)方法,就使用這個(gè)方法名向表中插入一條記錄,成功插入則獲取鎖,刪除對(duì)應(yīng)的行就是鎖釋放。
//鎖記錄表
CREATE TABLE `lock_info` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`name` varchar(64) NOT NULL COMMENT '方法名',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_name` (`method_name`)
) ENGINE=InnoD
這里主要是用name字段作為唯一索引來實(shí)現(xiàn),唯一索引保證了該記錄的唯一性,鎖釋放就直接刪掉該條記錄就行了。
缺點(diǎn)也很多:
- 1. 數(shù)據(jù)庫(kù)是單點(diǎn),非常依賴數(shù)據(jù)庫(kù)的可用性
- 2. 需要額外自己維護(hù)TTL
- 3. 在高并發(fā)常見下數(shù)據(jù)庫(kù)讀寫是非常緩慢
這里我們就不用過多的文字了,現(xiàn)實(shí)中我們更多的是用基于內(nèi)存存儲(chǔ)來實(shí)現(xiàn)分布式鎖。
基于Redis
面試官問:你了解分布式鎖嗎?想必絕大部分面試者都會(huì)說關(guān)于Redis實(shí)現(xiàn)分布式鎖的方式,OK,進(jìn)入正題【基于Redis分布式鎖】
Redis 的分布式鎖, setnx 命令并設(shè)置過期時(shí)間就行嗎?
setnx lkey lvalue expire lockKey 30
正常情況下是可以的,但是這里有個(gè)問題,雖然setnx是原子性的,但是setnx + expire就不是了,也就是說setnx和expire是分兩步執(zhí)行的,【加鎖和超時(shí)】?jī)蓚€(gè)操作是分開的,如果expire執(zhí)行失敗了,那么鎖同樣得不到釋放。
關(guān)于為什么要加鎖和超時(shí)時(shí)間的設(shè)定在文章開頭【避免死鎖】有提到,不明白的可以多看看。
Redis正確的加鎖命令是什么?
//保證原子性執(zhí)行命令
SET lKey randId NX PX 30000
randId是由客戶端生成的一個(gè)隨機(jī)字符串,該客戶端加鎖時(shí)具有唯一性,主要是為了避免釋放別人的鎖。
我們來看這么一樣流程,如下圖:
圖片
- 1. Client1 獲取鎖成功。
- 2. 由于Client1 業(yè)務(wù)處理時(shí)間過長(zhǎng), 鎖過期時(shí)間到了,鎖自動(dòng)釋放了
- 3. Client2 獲取到了對(duì)應(yīng)同一個(gè)資源的鎖。
- 4. Client1 業(yè)務(wù)處理完成,釋放鎖,但是釋放掉了Client2 持有的鎖。
- 5. 而Client3此時(shí)還能獲得鎖,同樣Client2此時(shí)持有鎖,都亂套了。??
而這個(gè)randId就可以在釋放鎖的時(shí)候避免了釋放別人的鎖,因?yàn)樵卺尫沛i的時(shí)候,Client需要先獲取到該鎖的值(randId),判斷是否相同后才能刪除。
if (redis.get(lKey).equals(randId)) {
redis.del(lockKey);
}
加鎖的時(shí)候需要原子性,釋放鎖的時(shí)候該怎么做到原子性???
這個(gè)問題很好,我們?cè)诩渔i的時(shí)候通過原子性命令避免了潛在的設(shè)置過期時(shí)間失敗問題,釋放鎖同樣是Get + Del兩條命令,這里同樣存在釋放別人鎖的問題。
腦瓜嗡嗡的??,咋那么多需要考慮的問題呀,看累了休息會(huì)??,咋們繼續(xù)往下看!
這里問題的根源在于:鎖的判斷在客戶端,釋放在服務(wù)端,如下圖:
圖片
所以 應(yīng)該將鎖的判斷和刪除都在redis服務(wù)端進(jìn)行,可以借助lua腳本保證原子性,釋放鎖的核心邏輯【GET、判斷、DEL】,寫成 Lua 腳,讓Redis執(zhí)行,這樣實(shí)現(xiàn)能保證這三步的原子性。
// 判斷鎖是自己的,才釋放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
如果Client1獲取到鎖后,因?yàn)闃I(yè)務(wù)問題需要較長(zhǎng)的處理時(shí)間,超過了鎖過期時(shí)間,該怎么辦?
既然業(yè)務(wù)執(zhí)行時(shí)間超過了鎖過期時(shí)間,那么我們可以給鎖續(xù)期呀,比如開啟一個(gè)守護(hù)進(jìn)程,定時(shí)監(jiān)測(cè)鎖的失效時(shí)間,在快要過期的時(shí)候,對(duì)鎖進(jìn)行自動(dòng)續(xù)期,重新設(shè)置過期時(shí)間。
Redisson框架中就實(shí)現(xiàn)了這個(gè),就要WatchDog(看門狗):加鎖時(shí)沒有指定加鎖時(shí)間時(shí)會(huì)啟用 watchdog 機(jī)制,默認(rèn)加鎖 30 秒,每 10 秒鐘檢查一次,如果存在就重新設(shè)置 過期時(shí)間為 30 秒(即 30 秒之后它就不再續(xù)期了)
圖片
嗯嗯,這應(yīng)該就比較穩(wěn)健了吧!??
嘿嘿,以上這些都是鎖在「單個(gè)」Redis 實(shí)例中可能產(chǎn)生的問題,確實(shí)單節(jié)點(diǎn)分布式鎖能解決大部分人的需求。但是通常都是用【Redis Cluster】或者【哨兵模式】這兩種方式實(shí)現(xiàn) Redis 的高可用,這就有主從同步問題發(fā)生。??
試想這樣的場(chǎng)景:
- 1. Client1請(qǐng)求Master加鎖成功
- 2. 然而Master異常宕機(jī),加鎖信息還未同步到從庫(kù)上(主從復(fù)制是異步的)
- 3. 此時(shí)從庫(kù)Slave1被哨兵提升為新主庫(kù),鎖信息不在新的主庫(kù)上(未同步到Slave1)
圖片
面對(duì)這種問題,Redis 的作者提出一種解決方 Redlock, 是基于多個(gè) Redis 節(jié)點(diǎn)(都是 Master)的一種實(shí)現(xiàn),該方案基于 2 個(gè)前提:
- 1. 不再需要部署從庫(kù)和哨兵實(shí)例,只部署主庫(kù)
- 2. 但主庫(kù)要部署多個(gè),官方推薦至少 5 個(gè)實(shí)例
Redlock加鎖流程:
1. Client先獲取「當(dāng)前時(shí)間戳T1」
2. Client依次向這 5 個(gè) Redis 實(shí)例發(fā)起加鎖請(qǐng)求(用前面講到的 SET 命令),且每個(gè)請(qǐng)求會(huì)設(shè)置超時(shí)時(shí)間(毫秒級(jí),要遠(yuǎn)小于鎖的有效時(shí)間),如果某一個(gè)實(shí)例加鎖失?。òňW(wǎng)絡(luò)超時(shí)、鎖被其它人持有等各種異常情況),就立即向下一個(gè) Redis 實(shí)例申請(qǐng)加鎖
3. 如果Client從 >=3 個(gè)(大多數(shù))以上 Redis 實(shí)例加鎖成功,則再次獲取「當(dāng)前時(shí)間戳T2」,如果 T2 - T1 < 鎖的過期時(shí)間,此時(shí),認(rèn)為客戶端加鎖成功,否則認(rèn)為加鎖失敗
4. 加鎖成功,去操作共享資源(例如修改 MySQL 某一行,或發(fā)起一個(gè) API 請(qǐng)求)
5. 加鎖失敗,Client向「全部節(jié)點(diǎn)」發(fā)起釋放鎖請(qǐng)求(前面講到的 Lua 腳本釋放鎖)
Redlock釋放鎖:
客戶端向所有 Redis 節(jié)點(diǎn)發(fā)起釋放鎖的操作
圖片
問題 1:為什么要在多個(gè)實(shí)例上加鎖?
本質(zhì)上為了容錯(cuò),我們看圖中的多個(gè)Master示例節(jié)點(diǎn),實(shí)際夠構(gòu)成了一個(gè)分布式系統(tǒng),分布式系統(tǒng)中總會(huì)有異常節(jié)點(diǎn),多個(gè)實(shí)例加鎖的話,即使部分實(shí)例異常宕機(jī),剩余的實(shí)例加鎖成功,整個(gè)鎖服務(wù)依舊可用!
問題 2:為什么步驟 3 加鎖成功后,還要計(jì)算加鎖的累計(jì)耗時(shí)?
加鎖操作的針對(duì)的是分布式中的多個(gè)節(jié)點(diǎn),所以耗時(shí)肯定是比單個(gè)實(shí)例耗時(shí)更,還要考慮網(wǎng)絡(luò)延遲、丟包、超時(shí)等情況發(fā)生,網(wǎng)絡(luò)請(qǐng)求次數(shù)越多,異常的概率越大。
所以即使 N/2+1 個(gè)節(jié)點(diǎn)加鎖成功,但如果加鎖的累計(jì)耗時(shí)已經(jīng)超過了鎖的過期時(shí)間,那么此時(shí)的鎖已經(jīng)沒有意義了
問題 3:為什么釋放鎖,要操作所有節(jié)點(diǎn)?
主要是為了保證清除節(jié)點(diǎn)異常情況導(dǎo)致殘留的鎖!
比如:在某一個(gè) Redis 節(jié)點(diǎn)加鎖時(shí),可能因?yàn)椤妇W(wǎng)絡(luò)原因」導(dǎo)致加鎖失敗。
或者客戶端在一個(gè) Redis 實(shí)例上加鎖成功,但在讀取響應(yīng)結(jié)果時(shí),網(wǎng)絡(luò)問題導(dǎo)致讀取失敗,那這把鎖其實(shí)已經(jīng)在 Redis 上加鎖成功了。
所以說釋放鎖的時(shí)候,不管以前有沒有加鎖成功,都要釋放所有節(jié)點(diǎn)的鎖。
這里有一個(gè)關(guān)于Redlock安全性的爭(zhēng)論,這里就一筆帶過吧,大家有興趣可以去看看:
Java面試365:RedLock紅鎖安全性爭(zhēng)論(上)4 贊同 · 0 評(píng)論文章
圖片
基于Etcd
Etcd是一個(gè)Go語言實(shí)現(xiàn)的非常可靠的kv存儲(chǔ)系統(tǒng),常在分布式系統(tǒng)中存儲(chǔ)著關(guān)鍵的數(shù)據(jù),通常應(yīng)用在配置中心、服務(wù)發(fā)現(xiàn)與注冊(cè)、分布式鎖等場(chǎng)景。
本文主要從分布式鎖的角度來看Etcd是如何實(shí)現(xiàn)分布式鎖的,Let's Go !
Etcd特性介紹:
- ? Lease機(jī)制:即租約機(jī)制(TTL,Time To Live),etcd可以為存儲(chǔ)的kv對(duì)設(shè)置租約,當(dāng)租約到期,kv將失效刪除;同時(shí)也支持續(xù)約,keepalive
- ? Revision機(jī)制:每個(gè)key帶有一個(gè)Revision屬性值,etcd每進(jìn)行一次事務(wù)對(duì)應(yīng)的全局Revision值都會(huì)+1,因此每個(gè)key對(duì)應(yīng)的Revision屬性值都是全局唯一的。通過比較Revision的大小就可以知道進(jìn)行寫操作的順序
- ? 在實(shí)現(xiàn)分布式鎖時(shí),多個(gè)程序同時(shí)搶鎖,根據(jù)Revision值大小依次獲得鎖,避免“驚群效應(yīng)”,實(shí)現(xiàn)公平鎖
- ? Prefix機(jī)制:也稱為目錄機(jī)制,可以根據(jù)前綴獲得該目錄下所有的key及其對(duì)應(yīng)的屬性值
- ? Watch機(jī)制:watch支持watch某個(gè)固定的key或者一個(gè)前綴目錄,當(dāng)watch的key發(fā)生變化,客戶端將收到通知
為什么這些特性就可以讓Etcd實(shí)現(xiàn)分布式鎖呢?因?yàn)镋tcd這些特性可以滿足實(shí)現(xiàn)分布式鎖的以下要求:
- ? 租約機(jī)制(Lease):用于支撐異常情況下的鎖自動(dòng)釋放能力
- ? 前綴和 Revision 機(jī)制:用于支撐公平獲取鎖和排隊(duì)等待的能力
- ? 監(jiān)聽機(jī)制(Watch):用于支撐搶鎖能力
- ? 集群模式:用于支撐鎖服務(wù)的高可用
有了這些知識(shí)理論我們一起看看Etcd是怎么實(shí)現(xiàn)分布式鎖的,因?yàn)槲易约阂彩荊olang開發(fā),這里我們也放一些代碼。
先看流程,再結(jié)合代碼注釋!
圖片
func main() {
config := clientv3.Config{
Endpoints: []string{"xxx.xxx.xxx.xxx:2379"},
DialTimeout: 5 * time.Second,
}
// 獲取客戶端連接
client, err := clientv3.New(config)
if err != nil {
fmt.Println(err)
return
}
// 1. 上鎖(創(chuàng)建租約,自動(dòng)續(xù)租,拿著租約去搶占一個(gè)key )
// 用于申請(qǐng)租約
lease := clientv3.NewLease(client)
// 申請(qǐng)一個(gè)10s的租約
leaseGrantResp, err := lease.Grant(context.TODO(), 10) //10s
if err != nil {
fmt.Println(err)
return
}
// 拿到租約的id
leaseID := leaseGrantResp.ID
// 準(zhǔn)備一個(gè)用于取消續(xù)租的context
ctx, cancelFunc := context.WithCancel(context.TODO())
// 確保函數(shù)退出后,自動(dòng)續(xù)租會(huì)停止
defer cancelFunc()
// 確保函數(shù)退出后,租約會(huì)失效
defer lease.Revoke(context.TODO(), leaseID)
// 自動(dòng)續(xù)租
keepRespChan, err := lease.KeepAlive(ctx, leaseID)
if err != nil {
fmt.Println(err)
return
}
// 處理續(xù)租應(yīng)答的協(xié)程
go func() {
select {
case keepResp := <-keepRespChan:
if keepRespChan == nil {
fmt.Println("lease has expired")
goto END
} else {
// 每秒會(huì)續(xù)租一次
fmt.Println("收到自動(dòng)續(xù)租應(yīng)答", keepResp.ID)
}
}
END:
}()
// if key 不存在,then設(shè)置它,else搶鎖失敗
kv := clientv3.NewKV(client)
// 創(chuàng)建事務(wù)
txn := kv.Txn(context.TODO())
// 如果key不存在
txn.If(clientv3.Compare(clientv3.CreateRevision("/cron/lock/job7"), "=", 0)).
Then(clientv3.OpPut("/cron/jobs/job7", "", clientv3.WithLease(leaseID))).
Else(clientv3.OpGet("/cron/jobs/job7")) //如果key存在
// 提交事務(wù)
txnResp, err := txn.Commit()
if err != nil {
fmt.Println(err)
return
}
// 判斷是否搶到了鎖
if !txnResp.Succeeded {
fmt.Println("鎖被占用了:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
return
}
// 2. 處理業(yè)務(wù)(鎖內(nèi),很安全)
fmt.Println("處理任務(wù)")
time.Sleep(5 * time.Second)
// 3. 釋放鎖(取消自動(dòng)續(xù)租,釋放租約)
// defer會(huì)取消續(xù)租,釋放鎖
}
不過clientv3提供的concurrency包也實(shí)現(xiàn)了分布式鎖,我們可以更便捷的實(shí)現(xiàn)分布式鎖,不過內(nèi)部實(shí)現(xiàn)邏輯差不多:
- 1. 首先concurrency.NewSession方法創(chuàng)建Session對(duì)象
- 2. 然后Session對(duì)象通過concurrency.NewMutex 創(chuàng)建了一個(gè)Mutex對(duì)象
- 3. 加鎖和釋放鎖分別調(diào)用Lock和UnLock
基于ZooKeeper
ZooKeeper 的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)就像一棵樹,這棵樹由節(jié)點(diǎn)組成,這種節(jié)點(diǎn)叫做 Znode
加鎖/釋放鎖的過程是這樣的
圖片
1. Client嘗試創(chuàng)建一個(gè) znode 節(jié)點(diǎn),比如/lock,比如Client1先到達(dá)就創(chuàng)建成功了,相當(dāng)于拿到了鎖
2. 其它的客戶端會(huì)創(chuàng)建失?。▃node 已存在),獲取鎖失敗。
3. Client2可以進(jìn)入一種等待狀態(tài),等待當(dāng)/lock 節(jié)點(diǎn)被刪除的時(shí)候,ZooKeeper 通過 watch 機(jī)制通知它
4. 持有鎖的Client1訪問共享資源完成后,將 znode 刪掉,鎖釋放掉了
5. Client2繼續(xù)完成獲取鎖操作,直到獲取到鎖為止
ZooKeeper不需要考慮過期時(shí)間,而是用【臨時(shí)節(jié)點(diǎn)】,Client拿到鎖之后,只要連接不斷,就會(huì)一直持有鎖。即使Client崩潰,相應(yīng)臨時(shí)節(jié)點(diǎn)Znode也會(huì)自動(dòng)刪除,保證了鎖釋放。
Zookeeper 是怎么檢測(cè)這個(gè)客戶端是否崩潰的呢?
每個(gè)客戶端都與 ZooKeeper 維護(hù)著一個(gè) Session,這個(gè) Session 依賴定期的心跳(heartbeat)來維持。
如果 Zookeeper 長(zhǎng)時(shí)間收不到客戶端的心跳,就認(rèn)為這個(gè) Session 過期了,也會(huì)把這個(gè)臨時(shí)節(jié)點(diǎn)刪除。
當(dāng)然這也并不是完美的解決方案
以下場(chǎng)景中Client1和Client2在窗口時(shí)間內(nèi)可能同時(shí)獲得鎖:
1. Client 1 創(chuàng)建了 znode 節(jié)點(diǎn)/lock,獲得了鎖。
2. Client 1 進(jìn)入了長(zhǎng)時(shí)間的 GC pause。(或者網(wǎng)絡(luò)出現(xiàn)問題、或者 zk 服務(wù)檢測(cè)心跳線程出現(xiàn)問題等等)
3. Client 1 連接到 ZooKeeper 的 Session 過期了。znode 節(jié)點(diǎn)/lock 被自動(dòng)刪除。
4. Client 2 創(chuàng)建了 znode 節(jié)點(diǎn)/lock,從而獲得了鎖。
5. Client 1 從 GC pause 中恢復(fù)過來,它仍然認(rèn)為自己持有鎖。
好,現(xiàn)在我們來總結(jié)一下 Zookeeper 在使用分布式鎖時(shí)優(yōu)劣:
Zookeeper 的優(yōu)點(diǎn):
- 1. 不需要考慮鎖的過期時(shí)間,使用起來比較方便
- 2. watch 機(jī)制,加鎖失敗,可以 watch 等待鎖釋放,實(shí)現(xiàn)樂觀鎖
缺點(diǎn):
- 1. 性能不如 Redis
- 2. 部署和運(yùn)維成本高
- 3. 客戶端與 Zookeeper 的長(zhǎng)時(shí)間失聯(lián),鎖被釋放問題
總結(jié)
文章內(nèi)容比較多,涉及到的知識(shí)點(diǎn)也很多,如果看一遍沒理解,那么建議你收藏一下多讀幾遍,構(gòu)建好對(duì)于分布式鎖你的情景結(jié)構(gòu)。
總結(jié)一下吧,本文主要總結(jié)了分布式鎖和使用方式,實(shí)現(xiàn)分布式鎖可以有多種方式。
數(shù)據(jù)庫(kù):通過創(chuàng)建一條唯一記錄來表示一個(gè)鎖,唯一記錄添加成功,鎖就創(chuàng)建成功,釋放鎖的話需要?jiǎng)h除記錄,但是很容易出現(xiàn)性能瓶頸,因此基本上不會(huì)使用數(shù)據(jù)庫(kù)作為分布式鎖。
Redis:Redis提供了高效的獲取鎖和釋放鎖的操作,而且結(jié)合Lua腳本,Redission等,有比較好的異常情況處理方式,因?yàn)槭腔趦?nèi)存的,讀寫效率也是非常高。
Etcd:利用租約(Lease),Watch,Revision機(jī)制,提供了一種簡(jiǎn)單實(shí)現(xiàn)的分布式鎖方式,集群模式讓Etcd能處理大量讀寫,性能出色,但是配置復(fù)雜,一致性問題也存在。
Zookeeper:利用ZooKeeper提供的節(jié)點(diǎn)同步功能來實(shí)現(xiàn)分布式鎖,而且不用設(shè)置過期時(shí)間,可以自動(dòng)的處理異常情況下的鎖釋放。
如果你的業(yè)務(wù)數(shù)據(jù)非常敏感,在使用分布式鎖時(shí),一定要注意這個(gè)問題,不能假設(shè)分布式鎖 100% 安全。
當(dāng)然也需要結(jié)合自己的業(yè)務(wù),可能大多數(shù)情況下我們還是使用Redis作為分布式鎖,一個(gè)是我們比較熟悉,然后性能和處理異常情況也有較多方式,我覺得滿足大多數(shù)業(yè)務(wù)場(chǎng)景就可以了。