學(xué)習(xí)完整課程請(qǐng)移步 互聯(lián)網(wǎng) Java 全棧工程師
本節(jié)視頻
- 【視頻】Dubbo 實(shí)現(xiàn)微服務(wù)架構(gòu)-Zookeeper-什么是分布式鎖1
- 【視頻】Dubbo 實(shí)現(xiàn)微服務(wù)架構(gòu)-Zookeeper-什么是分布式鎖2
- 【視頻】Dubbo 實(shí)現(xiàn)微服務(wù)架構(gòu)-Zookeeper-什么是分布式鎖3
概述
為了防止分布式系統(tǒng)中的多個(gè)進(jìn)程之間相互干擾堂油,我們需要一種分布式協(xié)調(diào)技術(shù)來對(duì)這些進(jìn)程進(jìn)行調(diào)度。而這個(gè)分布式協(xié)調(diào)技術(shù)的核心就是來實(shí)現(xiàn)這個(gè)分布式鎖。
為什么要使用分布式鎖
- 成員變量 A 存在 JVM1猾警、JVM2拢锹、JVM3 三個(gè) JVM 內(nèi)存中
- 成員變量 A 同時(shí)都會(huì)在 JVM 分配一塊內(nèi)存赖临,三個(gè)請(qǐng)求發(fā)過來同時(shí)對(duì)這個(gè)變量操作鲤脏,顯然結(jié)果是不對(duì)的
- 不是同時(shí)發(fā)過來秸歧,三個(gè)請(qǐng)求分別操作三個(gè)不同 JVM 內(nèi)存區(qū)域的數(shù)據(jù)涣脚,變量 A 之間不存在共享,也不具有可見性寥茫,處理的結(jié)果也是不對(duì)的
注:該成員變量 A 是一個(gè)有狀態(tài)的對(duì)象
如果我們業(yè)務(wù)中確實(shí)存在這個(gè)場(chǎng)景的話遣蚀,我們就需要一種方法解決這個(gè)問題,這就是分布式鎖要解決的問題
分布式鎖應(yīng)該具備哪些條件
- 在分布式系統(tǒng)環(huán)境下纱耻,一個(gè)方法在同一時(shí)間只能被一個(gè)機(jī)器的一個(gè)線程執(zhí)行
- 高可用的獲取鎖與釋放鎖
- 高性能的獲取鎖與釋放鎖
- 具備可重入特性(可理解為重新進(jìn)入芭梯,由多于一個(gè)任務(wù)并發(fā)使用,而不必?fù)?dān)心數(shù)據(jù)錯(cuò)誤)
- 具備鎖失效機(jī)制弄喘,防止死鎖
- 具備非阻塞鎖特性玖喘,即沒有獲取到鎖將直接返回獲取鎖失敗
分布式鎖的實(shí)現(xiàn)有哪些
- Memcached:利用 Memcached 的
add
命令。此命令是原子性操作蘑志,只有在key
不存在的情況下累奈,才能add
成功贬派,也就意味著線程得到了鎖。 - Redis:和 Memcached 的方式類似澎媒,利用 Redis 的
setnx
命令搞乏。此命令同樣是原子性操作,只有在key
不存在的情況下戒努,才能set
成功请敦。 - Zookeeper:利用 Zookeeper 的順序臨時(shí)節(jié)點(diǎn),來實(shí)現(xiàn)分布式鎖和等待隊(duì)列储玫。Zookeeper 設(shè)計(jì)的初衷侍筛,就是為了實(shí)現(xiàn)分布式鎖服務(wù)的。
- Chubby:Google 公司實(shí)現(xiàn)的粗粒度分布式鎖服務(wù)撒穷,底層利用了 Paxos 一致性算法匣椰。
通過 Redis 分布式鎖的實(shí)現(xiàn)理解基本概念
分布式鎖實(shí)現(xiàn)的三個(gè)核心要素:
加鎖
最簡(jiǎn)單的方法是使用 setnx
命令。key
是鎖的唯一標(biāo)識(shí)端礼,按業(yè)務(wù)來決定命名禽笑。比如想要給一種商品的秒殺活動(dòng)加鎖,可以給 key
命名為 “l(fā)ock_sale_商品ID” 齐媒。而 value
設(shè)置成什么呢?我們可以姑且設(shè)置成 1
纷跛。加鎖的偽代碼如下:
setnx(lock_sale_商品ID喻括,1)
當(dāng)一個(gè)線程執(zhí)行 setnx
返回 1
,說明 key
原本不存在贫奠,該線程成功得到了鎖唬血;當(dāng)一個(gè)線程執(zhí)行 setnx
返回 0
,說明 key
已經(jīng)存在唤崭,該線程搶鎖失敗拷恨。
解鎖
有加鎖就得有解鎖。當(dāng)?shù)玫芥i的線程執(zhí)行完任務(wù)谢肾,需要釋放鎖腕侄,以便其他線程可以進(jìn)入。釋放鎖的最簡(jiǎn)單方式是執(zhí)行 del
指令芦疏,偽代碼如下:
del(lock_sale_商品ID)
釋放鎖之后冕杠,其他線程就可以繼續(xù)執(zhí)行 setnx
命令來獲得鎖。
鎖超時(shí)
鎖超時(shí)是什么意思呢酸茴?如果一個(gè)得到鎖的線程在執(zhí)行任務(wù)的過程中掛掉分预,來不及顯式地釋放鎖,這塊資源將會(huì)永遠(yuǎn)被鎖仔胶础(死鎖)笼痹,別的線程再也別想進(jìn)來配喳。所以,setnx
的 key
必須設(shè)置一個(gè)超時(shí)時(shí)間凳干,以保證即使沒有被顯式釋放晴裹,這把鎖也要在一定時(shí)間后自動(dòng)釋放。setnx
不支持超時(shí)參數(shù)纺座,所以需要額外的指令息拜,偽代碼如下:
expire(lock_sale_商品ID, 30)
綜合偽代碼如下:
if(setnx(lock_sale_商品ID净响,1) == 1){
expire(lock_sale_商品ID少欺,30)
try {
do something ......
} finally {
del(lock_sale_商品ID)
}
}
存在什么問題
以上偽代碼中存在三個(gè)致命問題
setnx
和 expire
的非原子性
設(shè)想一個(gè)極端場(chǎng)景,當(dāng)某線程執(zhí)行 setnx
馋贤,成功得到了鎖:
setnx
剛執(zhí)行成功赞别,還未來得及執(zhí)行 expire
指令,節(jié)點(diǎn) 1 掛掉了配乓。
這樣一來仿滔,這把鎖就沒有設(shè)置過期時(shí)間,變成死鎖犹芹,別的線程再也無法獲得鎖了崎页。
怎么解決呢?setnx
指令本身是不支持傳入超時(shí)時(shí)間的腰埂,set
指令增加了可選參數(shù)飒焦,偽代碼如下:
set(lock_sale_商品ID,1屿笼,30牺荠,NX)
這樣就可以取代 setnx
指令。
del
導(dǎo)致誤刪
又是一個(gè)極端場(chǎng)景驴一,假如某線程成功得到了鎖休雌,并且設(shè)置的超時(shí)時(shí)間是 30 秒。
如果某些原因?qū)е戮€程 A 執(zhí)行的很慢很慢肝断,過了 30 秒都沒執(zhí)行完杈曲,這時(shí)候鎖過期自動(dòng)釋放,線程 B 得到了鎖胸懈。
隨后鱼蝉,線程 A 執(zhí)行完了任務(wù),線程 A 接著執(zhí)行 del
指令來釋放鎖。但這時(shí)候線程 B 還沒執(zhí)行完,線程A實(shí)際上 刪除的是線程 B 加的鎖
牛郑。
怎么避免這種情況呢?可以在 del
釋放鎖之前做一個(gè)判斷洁奈,驗(yàn)證當(dāng)前的鎖是不是自己加的鎖间唉。至于具體的實(shí)現(xiàn),可以在加鎖的時(shí)候把當(dāng)前的線程 ID 當(dāng)做 value
利术,并在刪除之前驗(yàn)證 key
對(duì)應(yīng)的 value
是不是自己線程的 ID呈野。
加鎖:
String threadId = Thread.currentThread().getId()
set(key,threadId 印叁,30被冒,NX)
解鎖:
if(threadId .equals(redisClient.get(key))){
del(key)
}
但是,這樣做又隱含了一個(gè)新的問題轮蜕,判斷和釋放鎖是兩個(gè)獨(dú)立操作昨悼,不是原子性。
出現(xiàn)并發(fā)的可能性
還是剛才第二點(diǎn)所描述的場(chǎng)景跃洛,雖然我們避免了線程 A 誤刪掉 key
的情況率触,但是同一時(shí)間有 A,B 兩個(gè)線程在訪問代碼塊汇竭,仍然是不完美的葱蝗。怎么辦呢?我們可以讓獲得鎖的線程開啟一個(gè)守護(hù)線程细燎,用來給快要過期的鎖“續(xù)航”两曼。
當(dāng)過去了 29 秒,線程 A 還沒執(zhí)行完玻驻,這時(shí)候守護(hù)線程會(huì)執(zhí)行 expire
指令悼凑,為這把鎖“續(xù)命”20 秒。守護(hù)線程從第 29 秒開始執(zhí)行击狮,每 20 秒執(zhí)行一次佛析。
當(dāng)線程 A 執(zhí)行完任務(wù)益老,會(huì)顯式關(guān)掉守護(hù)線程彪蓬。
另一種情況,如果節(jié)點(diǎn) 1 忽然斷電捺萌,由于線程 A 和守護(hù)線程在同一個(gè)進(jìn)程档冬,守護(hù)線程也會(huì)停下。這把鎖到了超時(shí)的時(shí)候桃纯,沒人給它續(xù)命酷誓,也就自動(dòng)釋放了。