一文了解分布式鎖
大多數(shù)互聯(lián)網(wǎng)系統(tǒng)都是分布式部署的,分布式部署確實(shí)能帶來性能和效率上的提升,但為此,我們就需要多解決一個(gè)分布式環(huán)境下,數(shù)據(jù)一致性的問題。
?
當(dāng)某個(gè)資源在多系統(tǒng)之間,具有共享性的時(shí)候,為了保證大家訪問這個(gè)資源數(shù)據(jù)是一致的,那么就必須要求在同一時(shí)刻只能被一個(gè)客戶端處理,不能并發(fā)的執(zhí)行,否者就會(huì)出現(xiàn)同一時(shí)刻有人寫有人讀,大家訪問到的數(shù)據(jù)就不一致了。
?
一、我們?yōu)槭裁葱枰植际芥i?
?
在單機(jī)時(shí)代,雖然不需要分布式鎖,但也面臨過類似的問題,只不過在單機(jī)的情況下,如果有多個(gè)線程要同時(shí)訪問某個(gè)共享資源的時(shí)候,我們可以采用線程間加鎖的機(jī)制,即當(dāng)某個(gè)線程獲取到這個(gè)資源后,就立即對(duì)這個(gè)資源進(jìn)行加鎖,當(dāng)使用完資源之后,再解鎖,其它線程就可以接著使用了。例如,在JAVA中,甚至專門提供了一些處理鎖機(jī)制的一些API(synchronize/Lock等)
?
但是到了分布式系統(tǒng)的時(shí)代,這種線程之間的鎖機(jī)制,就沒作用了,系統(tǒng)可能會(huì)有多份并且部署在不同的機(jī)器上,這些資源已經(jīng)不是在線程之間共享了,而是屬于進(jìn)程之間共享的資源。
?
因此,為了解決這個(gè)問題,我們就必須引入「分布式鎖」。
?
分布式鎖,是指在分布式的部署環(huán)境下,通過鎖機(jī)制來讓多客戶端互斥的對(duì)共享資源進(jìn)行訪問。
?
分布式鎖要滿足哪些要求呢?
?
排他性:在同一時(shí)間只會(huì)有一個(gè)客戶端能獲取到鎖,其它客戶端無法同時(shí)獲取
避免死鎖:這把鎖在一段有限的時(shí)間之后,一定會(huì)被釋放(正常釋放或異常釋放)
高可用:獲取或釋放鎖的機(jī)制必須高可用且性能佳
講完了背景和理論,那我們接下來再看一下分布式鎖的具體分類和實(shí)際運(yùn)用。
?
二、分布式鎖的實(shí)現(xiàn)方式有哪些?
?
目前主流的有三種,從實(shí)現(xiàn)的復(fù)雜度上來看,從上往下難度依次增加:
?
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)
基于Redis實(shí)現(xiàn)
基于ZooKeeper實(shí)現(xiàn)
無論哪種方式,其實(shí)都不完美,依舊要根據(jù)咱們業(yè)務(wù)的實(shí)際場(chǎng)景來選擇。
?
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn):
基于數(shù)據(jù)庫(kù)來做分布式鎖的話,通常有兩種做法:
基于數(shù)據(jù)庫(kù)的樂觀鎖
基于數(shù)據(jù)庫(kù)的悲觀鎖
?
我們先來看一下如何基于「樂觀鎖」來實(shí)現(xiàn):
?
樂觀鎖機(jī)制其實(shí)就是在數(shù)據(jù)庫(kù)表中引入一個(gè)版本號(hào)(version)字段來實(shí)現(xiàn)的。
?
當(dāng)我們要從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)的時(shí)候,同時(shí)把這個(gè)version字段也讀出來,如果要對(duì)讀出來的數(shù)據(jù)進(jìn)行更新后寫回?cái)?shù)據(jù)庫(kù),則需要將version加1,同時(shí)將新的數(shù)據(jù)與新的version更新到數(shù)據(jù)表中,且必須在更新的時(shí)候同時(shí)檢查目前數(shù)據(jù)庫(kù)里version值是不是之前的那個(gè)version,如果是,則正常更新。如果不是,則更新失敗,說明在這個(gè)過程中有其它的進(jìn)程去更新過數(shù)據(jù)了。
?
下面找圖舉例,

?
如圖,假設(shè)同一個(gè)賬戶,用戶A和用戶B都要去進(jìn)行取款操作,賬戶的原始余額是2000,用戶A要去取1500,用戶B要去取1000,如果沒有鎖機(jī)制的話,在并發(fā)的情況下,可能會(huì)出現(xiàn)余額同時(shí)被扣1500和1000,導(dǎo)致最終余額的不正確甚至是負(fù)數(shù)。但如果這里用到樂觀鎖機(jī)制,當(dāng)兩個(gè)用戶去數(shù)據(jù)庫(kù)中讀取余額的時(shí)候,除了讀取到2000余額以外,還讀取了當(dāng)前的版本號(hào)version=1,等用戶A或用戶B去修改數(shù)據(jù)庫(kù)余額的時(shí)候,無論誰(shuí)先操作,都會(huì)將版本號(hào)加1,即version=2,那么另外一個(gè)用戶去更新的時(shí)候就發(fā)現(xiàn)版本號(hào)不對(duì),已經(jīng)變成2了,不是當(dāng)初讀出來時(shí)候的1,那么本次更新失敗,就得重新去讀取最新的數(shù)據(jù)庫(kù)余額。
?
通過上面這個(gè)例子可以看出來,使用「樂觀鎖」機(jī)制,必須得滿足:
(1)鎖服務(wù)要有遞增的版本號(hào)version
(2)每次更新數(shù)據(jù)的時(shí)候都必須先判斷版本號(hào)對(duì)不對(duì),然后再寫入新的版本號(hào)
?
我們?cè)賮砜匆幌氯绾位凇副^鎖」來實(shí)現(xiàn):
悲觀鎖也叫作排它鎖,在Mysql中是基于 for update 來實(shí)現(xiàn)加鎖的,例如:
//鎖定的方法-偽代碼public boolean lock(){
? ?connection.setAutoCommit(false) ? ?for(){
? ? ? ?result =
? ? ? ?select * from user where
? ? ? ?id = 100 for update; ? ? ? ?if(result){ ? ? ? ? //結(jié)果不為空,
? ? ? ?//則說明獲取到了鎖
? ? ? ? ? ?return true;
? ? ? ?} ? ? ? ?//沒有獲取到鎖,繼續(xù)獲取
? ? ? ?sleep(1000);
? ?} ? ?return false;
}//釋放鎖-偽代碼connection.commit();
上面的示例中,user表中,id是主鍵,通過 for update 操作,數(shù)據(jù)庫(kù)在查詢的時(shí)候就會(huì)給這條記錄加上排它鎖。
(需要注意的是,在InnoDB中只有字段加了索引的,才會(huì)是行級(jí)鎖,否者是表級(jí)鎖,所以這個(gè)id字段要加索引)
當(dāng)這條記錄加上排它鎖之后,其它線程是無法操作這條記錄的。
那么,這樣的話,我們就可以認(rèn)為獲得了排它鎖的這個(gè)線程是擁有了分布式鎖,然后就可以執(zhí)行我們想要做的業(yè)務(wù)邏輯,當(dāng)邏輯完成之后,再調(diào)用上述釋放鎖的語(yǔ)句即可。
?
基于Redis實(shí)現(xiàn)
?
基于Redis實(shí)現(xiàn)的鎖機(jī)制,主要是依賴redis自身的原子操作,例如:
SET user_key user_value NX PX 100
?
redis從2.6.12版本開始,SET命令才支持這些參數(shù):
?
NX:只在在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作,SET key value NX 效果等同于 SETNX key value?
?
PX millisecond:設(shè)置鍵的過期時(shí)間為millisecond毫秒,當(dāng)超過這個(gè)時(shí)間后,設(shè)置的鍵會(huì)自動(dòng)失效
?
上述代碼示例是指,當(dāng)redis中不存在user_key這個(gè)鍵的時(shí)候,才會(huì)去設(shè)置一個(gè)user_key鍵,并且給這個(gè)鍵的值設(shè)置為 user_value,且這個(gè)鍵的存活時(shí)間為100ms
?
為什么這個(gè)命令可以幫我們實(shí)現(xiàn)鎖機(jī)制呢?
?
因?yàn)檫@個(gè)命令是只有在某個(gè)key不存在的時(shí)候,才會(huì)執(zhí)行成功。那么當(dāng)多個(gè)進(jìn)程同時(shí)并發(fā)的去設(shè)置同一個(gè)key的時(shí)候,就永遠(yuǎn)只會(huì)有一個(gè)進(jìn)程成功。
?
當(dāng)某個(gè)進(jìn)程設(shè)置成功之后,就可以去執(zhí)行業(yè)務(wù)邏輯了,等業(yè)務(wù)邏輯執(zhí)行完畢之后,再去進(jìn)行解鎖。
?
解鎖很簡(jiǎn)單,只需要?jiǎng)h除這個(gè)key就可以了,不過刪除之前需要判斷,這個(gè)key對(duì)應(yīng)的value是當(dāng)初自己設(shè)置的那個(gè)。
?
另外,針對(duì)redis集群模式的分布式鎖,可以采用redis的Redlock機(jī)制。
?
基于ZooKeeper實(shí)現(xiàn)
?
其實(shí)基于ZooKeeper,就是使用它的臨時(shí)有序節(jié)點(diǎn)來實(shí)現(xiàn)的分布式鎖。
?
原理就是:當(dāng)某客戶端要進(jìn)行邏輯的加鎖時(shí),就在zookeeper上的某個(gè)指定節(jié)點(diǎn)的目錄下,去生成一個(gè)唯一的臨時(shí)有序節(jié)點(diǎn), 然后判斷自己是否是這些有序節(jié)點(diǎn)中序號(hào)最小的一個(gè),如果是,則算是獲取了鎖。如果不是,則說明沒有獲取到鎖,那么就需要在序列中找到比自己小的那個(gè)節(jié)點(diǎn),并對(duì)其調(diào)用exist()方法,對(duì)其注冊(cè)事件監(jiān)聽,當(dāng)監(jiān)聽到這個(gè)節(jié)點(diǎn)被刪除了,那就再去判斷一次自己當(dāng)初創(chuàng)建的節(jié)點(diǎn)是否變成了序列中最小的。如果是,則獲取鎖,如果不是,則重復(fù)上述步驟。
?
當(dāng)釋放鎖的時(shí)候,只需將這個(gè)臨時(shí)節(jié)點(diǎn)刪除即可。

如圖,locker是一個(gè)持久節(jié)點(diǎn),node_1/node_2/…/node_n 就是上面說的臨時(shí)節(jié)點(diǎn),由客戶端client去創(chuàng)建的。
?
client_1/client_2/…/clien_n 都是想去獲取鎖的客戶端。以client_1為例,它想去獲取分布式鎖,則需要跑到locker下面去創(chuàng)建臨時(shí)節(jié)點(diǎn)(假如是node_1)創(chuàng)建完畢后,看一下自己的節(jié)點(diǎn)序號(hào)是否是locker下面最小的,如果是,則獲取了鎖。如果不是,則去找到比自己小的那個(gè)節(jié)點(diǎn)(假如是node_2),找到后,就監(jiān)聽node_2,直到node_2被刪除,那么就開始再次判斷自己的node_1是不是序列中最小的,如果是,則獲取鎖,如果還不是,則繼續(xù)找一下一個(gè)節(jié)點(diǎn)。
?
以上,就講完了為什么我們需要分布式鎖這個(gè)技術(shù),以及分布式鎖中常見的三種機(jī)制。