引題
比如在同一個節(jié)點上,兩個線程并發(fā)的操作A的賬戶,都是取錢,如果不加鎖,A的賬戶可能會出現(xiàn)負(fù)數(shù),正確的方式是對賬戶acount進(jìn)行加鎖,即使用synchronized關(guān)鍵字,對其進(jìn)行加鎖后,當(dāng)有線程訪問時,會獲得鎖,并對其資源進(jìn)行修改操作,其他的線程只有當(dāng)該線程修改完成后并且釋放鎖,才能對其訪問,這種加鎖--修改--釋放鎖的模式就解決了多個線程同時修改資源而造成的錯誤。
但是,在分布式集群系統(tǒng)中,兩個節(jié)點上的兩個進(jìn)程需要執(zhí)行同一段代碼塊,訪問同一個臨界資源,如何保證不同節(jié)點的進(jìn)程同步執(zhí)行呢?
一、什么是分布式鎖(What)
什么是鎖
在單進(jìn)程的系統(tǒng)中,當(dāng)存在多個線程可以同時改變某個變量(可變共享變量)時,就需要對變量或代碼塊做同步,使其在修改這種變量時能夠線性執(zhí)行消除并發(fā)修改變量。
而同步的本質(zhì)是通過鎖來實現(xiàn)的。為了實現(xiàn)多個線程在一個時刻同一個代碼塊只能有一個線程可執(zhí)行,那么需要在某個地方做個標(biāo)記,這個標(biāo)記必須每個線程都能看到,當(dāng)標(biāo)記不存在時可以設(shè)置該標(biāo)記,其余后續(xù)線程發(fā)現(xiàn)已經(jīng)有標(biāo)記了則等待擁有標(biāo)記的線程結(jié)束同步代碼塊取消標(biāo)記后再去嘗試設(shè)置標(biāo)記。這個標(biāo)記可以理解為鎖。
不同地方實現(xiàn)鎖的方式也不一樣,只要能滿足所有線程都能看得到標(biāo)記即可。如java中synchronize是在對象頭設(shè)置標(biāo)記,Lock接口的實現(xiàn)類基本上都只是某一個volitile修飾的int型變量其保證每個線程都能擁有對該int的可見性和原子修改,linux內(nèi)核中也是利用互斥量或信號量等內(nèi)存數(shù)據(jù)做標(biāo)記。
分布式情況
此處主要指集群模式下,多個相同服務(wù)同時開啟.
分布式與單機(jī)情況下最大的不同在于其不是多線程而是多進(jìn)程。
多線程由于可以共享堆內(nèi)存,因此可以簡單的采取內(nèi)存作為標(biāo)記存儲位置。而進(jìn)程之間甚至可能都不在同一臺物理機(jī)上,因此需要將標(biāo)記存儲在一個所有進(jìn)程都能看到的地方。
分布式鎖
當(dāng)在分布式模型下,數(shù)據(jù)可能只有一份,此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進(jìn)程數(shù)。
與單機(jī)模式下的鎖不同,分布式鎖不僅需要保證進(jìn)程可見,還需要考慮進(jìn)程與鎖之間的網(wǎng)絡(luò)問題。(分布式情況下之所以問題變得復(fù)雜,主要就是需要考慮到網(wǎng)絡(luò)的延時和不可靠)
分布式鎖還是可以將標(biāo)記存在內(nèi)存,只是該內(nèi)存不是某個進(jìn)程分配的內(nèi)存而是公共內(nèi)存如Redis、Memcache。至于利用數(shù)據(jù)庫、文件等做鎖與單機(jī)的實現(xiàn)是一樣的,只要保證標(biāo)記能互斥就行。
二、為什么需要分布式鎖(Why)
哪些場景需要用
場景一:比較敏感的數(shù)據(jù)比如金額修改,同一時間只能有一個人操作,想象下2個人同時修改金額,一個加金額一個減金額,為了防止同時操作造成數(shù)據(jù)不一致,需要鎖,如果是數(shù)據(jù)庫需要的就是行鎖或表鎖,如果是在集群里,多個客戶端同時修改一個共享的數(shù)據(jù)就需要分布式鎖。
場景二:比如多臺機(jī)器都可以定時執(zhí)行某個任務(wù),如果限制任務(wù)每次只能被一臺機(jī)器執(zhí)行,不能重復(fù)執(zhí)行,就可以用分布式鎖來做標(biāo)記。
場景三:比如秒殺場景,要求并發(fā)量很高,那么同一件商品只能被一個用戶搶到,那么就可以使用分布式鎖實現(xiàn)。
三、分布式鎖的幾種實現(xiàn)方式(How)
針對分布式鎖的實現(xiàn),目前比較常用的有以下幾種方案:
1、基于數(shù)據(jù)庫實現(xiàn)分布式鎖?
2、基于緩存(redis,memcached,tair)實現(xiàn)分布式鎖
?3、基于Zookeeper實現(xiàn)分布式鎖
在分析這幾種實現(xiàn)方案之前我們先來想一下,我們需要的分布式鎖應(yīng)該是怎么樣的?(這里以方法鎖為例,資源鎖同理)
可以保證在分布式部署的應(yīng)用集群中,同一個方法在同一時間只能被一臺機(jī)器上的一個進(jìn)程執(zhí)行。
A 這把鎖要是一把可重入鎖(避免死鎖)
B 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
C 有高可用的獲取鎖和釋放鎖功能
D 獲取鎖和釋放鎖的性能要好
1、基于數(shù)據(jù)庫實現(xiàn)分布式鎖
基于數(shù)據(jù)庫表
要實現(xiàn)分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表,然后通過操作該表中的數(shù)據(jù)來實現(xiàn)了。
當(dāng)我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
創(chuàng)建這樣一張數(shù)據(jù)庫表:
CREATETABLE`methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT ''COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數(shù)據(jù)時間,自動生成',
PRIMARYKEY(`id`),
UNIQUEKEY `uidx_method_name` (`method_name `)USING BTREE
)ENGINE=InnoDB DEFAULTCHARSET=utf8 COMMENT='鎖定中的方法';
當(dāng)我們想要鎖住某個方法時,執(zhí)行以下SQL:
insert into methodLock(method_name ,desc) values(‘method_name’ ,‘desc’)
因為我們對method_name做了唯一性約束,這里如果有多個請求同時提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會保證只有一個操作可以成功,那么我們就可以認(rèn)為操作成功的那個線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容。
當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,需要執(zhí)行以下Sql:
delete from methodLock where method_name ='method_name'
上面這種簡單的實現(xiàn)有以下幾個問題:
1、這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個單點,一旦數(shù)據(jù)庫掛掉,會導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
3、這把鎖只能是非阻塞的,因為數(shù)據(jù)的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進(jìn)入排隊隊列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數(shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
當(dāng)然,我們也可以有其他方式解決上面的問題。
數(shù)據(jù)庫是單點?搞兩個數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫上。
沒有失效時間?只要做一個定時任務(wù),每隔一定時間把數(shù)據(jù)庫中的超時數(shù)據(jù)清理一遍。
非阻塞的?搞一個while循環(huán),直到insert成功再返回成功。
非重入的?在數(shù)據(jù)庫表中加個字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時候先查詢數(shù)據(jù)庫,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給他就可以了。
基于數(shù)據(jù)庫排他鎖
除了可以通過增刪操作數(shù)據(jù)表中的記錄以外,其實還可以借助數(shù)據(jù)庫自帶的鎖來實現(xiàn)分布式的鎖。
我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫表。可以通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖。 基于MySql的InnoDB引擎,可以使用以下方法來實現(xiàn)加鎖操作:
public boolean lock(){
????connection.setAutoCommit(false)
????while(true){
????????try{
????????????????result= select * from methodLock where method_name=xxx for update;
????????????????if(result==null){
????????????????return true;
????????????}
????????}catch(Exception e){
????}
? ? sleep(1000);
}
????return false;
}
在查詢語句后面增加for update,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖(這里再多提一句,InnoDB引擎在加鎖的時候,只有通過索引進(jìn)行檢索的時候才會使用行級鎖,否則會使用表級鎖。這里我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要創(chuàng)建成唯一索引,否則會出現(xiàn)多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數(shù)類型也加上。)。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
我們可以認(rèn)為獲得排它鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過以下方法解鎖:
public void unlock(){
????connection.commit();
}
通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖?for update語句會在執(zhí)行成功后立即返回,在執(zhí)行失敗時一直處于阻塞狀態(tài),直到成功。
鎖定之后服務(wù)宕機(jī),無法釋放?使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫會自己把鎖釋放掉。
但是還是無法解決數(shù)據(jù)庫單點和可重入問題。
這種方式還有一個問題,就是我們要使用排他鎖來進(jìn)行分布式鎖的lock,那么一個排他鎖長時間不提交,就會占用數(shù)據(jù)庫連接。一旦類似的連接變得多了,就可能把數(shù)據(jù)庫連接池?fù)伪?/p>
總結(jié)
總結(jié)一下使用數(shù)據(jù)庫來實現(xiàn)分布式鎖的方式,這兩種方式都是依賴數(shù)據(jù)庫的一張表,一種是通過表中的記錄的存在情況確定當(dāng)前是否有鎖存在,另外一種是通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖。
數(shù)據(jù)庫實現(xiàn)分布式鎖的優(yōu)點:
1、直接借助數(shù)據(jù)庫,容易理解。
數(shù)據(jù)庫實現(xiàn)分布式鎖的缺點:
1、會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越復(fù)雜。
2、操作數(shù)據(jù)庫需要一定的開銷,性能問題需要考慮。
3、使用數(shù)據(jù)庫的行級鎖并不一定靠譜,尤其是當(dāng)我們的鎖表并不大的時候。
2?基于緩存實現(xiàn)分布式鎖
相比較于基于數(shù)據(jù)庫實現(xiàn)分布式鎖的方案來說,基于緩存來實現(xiàn)在性能方面會表現(xiàn)的更好一點。而且很多緩存是可以集群部署的,可以解決單點問題。
目前有很多成熟的緩存產(chǎn)品,包括Redis,memcached以及阿里內(nèi)部用的Tair。
這里以Redis為例:
Redis分布式鎖的基本流程并不難理解,但要想寫得盡善盡美,也并不是那么容易。在這里,我們需要先了解分布式鎖實現(xiàn)的三個核心要素:
1.加鎖
最簡單的方法是使用setnx命令。key是鎖的唯一標(biāo)識,按業(yè)務(wù)來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名為 “l(fā)ock_sale_商品ID” 。而value設(shè)置成什么呢?我們可以姑且設(shè)置成1。加鎖的偽代碼如下:
setnx(key,1)
當(dāng)一個線程執(zhí)行setnx返回1,說明key原本不存在,該線程成功得到了鎖;當(dāng)一個線程執(zhí)行setnx返回0,說明key已經(jīng)存在,該線程搶鎖失敗。
2.解鎖
有加鎖就得有解鎖。當(dāng)?shù)玫芥i的線程執(zhí)行完任務(wù),需要釋放鎖,以便其他線程可以進(jìn)入。釋放鎖的最簡單方式是執(zhí)行del指令,偽代碼如下:
del(key)
釋放鎖之后,其他線程就可以繼續(xù)執(zhí)行setnx命令來獲得鎖。
3.鎖超時
鎖超時是什么意思呢?如果一個得到鎖的線程在執(zhí)行任務(wù)的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠(yuǎn)被鎖住,別的線程再也別想進(jìn)來。
所以,setnx的key必須設(shè)置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間后自動釋放。setnx不支持超時參數(shù),所以需要額外的指令,偽代碼如下:
expire(key, 30)
綜合起來,我們分布式鎖實現(xiàn)的第一版?zhèn)未a如下:
if(setnx(key,1) == 1){? ??
????????expire(key,30)? ??
????????try {? ? ? ??
????????????do something ......? ??
????????} finally {? ? ? ??
????????????del(key)? ? }
}
上面的偽代碼中,存在著三個致命問題:
1、setnx和expire的非原子性
設(shè)想一個極端場景,當(dāng)某線程執(zhí)行setnx,成功得到了鎖:
setnx剛執(zhí)行成功,還未來得及執(zhí)行expire指令,節(jié)點1 突然掛掉了。
這樣一來,這把鎖就沒有設(shè)置過期時間,變得“長生不老”,別的線程再也無法獲得鎖了。
怎么解決呢?setnx指令本身是不支持傳入超時時間的,幸好Redis 2.6.12以上版本為set指令增加了可選參數(shù),偽代碼如下:
set(key,1,30,NX)
這樣就可以取代setnx指令。
2、del 導(dǎo)致誤刪
又是一個極端場景,假如某線程成功得到了鎖,并且設(shè)置的超時時間是30秒。
如果某些原因?qū)е戮€程A執(zhí)行的很慢很慢,過了30秒都沒執(zhí)行完,這時候鎖過期自動釋放,線程B得到了鎖。
隨后,線程A執(zhí)行完了任務(wù),線程A接著執(zhí)行del指令來釋放鎖。但這時候線程B還沒執(zhí)行完,線程A實際上刪除的是線程B加的鎖。
怎么避免這種情況呢?可以在del釋放鎖之前做一個判斷,驗證當(dāng)前的鎖是不是自己加的鎖。
至于具體的實現(xiàn),可以在加鎖的時候把當(dāng)前的線程ID當(dāng)做value,并在刪除之前驗證key對應(yīng)的value是不是自己線程的ID。
加鎖:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
解鎖:
if(threadId .equals(redisClient.get(key))){? ??
????del(key)}
但是,這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操作,不是原子性。
我們都是追求極致的程序員,所以這一塊要用Lua腳本來實現(xiàn):
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
這樣一來,驗證和刪除過程就是原子操作了。
3.、出現(xiàn)并發(fā)的可能性
還是剛才第二點所描述的場景,雖然我們避免了線程A誤刪掉key的情況,但是同一時間有A,B兩個線程在訪問代碼塊,相當(dāng)于鎖的作用失效了。
怎么辦呢?我們可以讓獲得鎖的線程開啟一個守護(hù)線程,用來給快要過期的鎖“續(xù)命”。
當(dāng)過去了29秒,線程A還沒執(zhí)行完,這時候守護(hù)線程會執(zhí)行expire指令,為這把鎖“續(xù)命”20秒。守護(hù)線程從第29秒開始執(zhí)行,每20秒執(zhí)行一次。
當(dāng)線程A執(zhí)行完任務(wù),會顯式關(guān)掉守護(hù)線程。
另一種情況,如果節(jié)點1忽然斷電,由于線程A和守護(hù)線程在同一個進(jìn)程,守護(hù)線程也會停下。這把鎖到了超時的時候,沒人給它續(xù)命,也就自動釋放了。
總結(jié)
可以使用緩存來代替數(shù)據(jù)庫來實現(xiàn)分布式鎖,這個可以提供更好的性能,同時,很多緩存服務(wù)都是集群部署的,可以避免單點問題。并且很多緩存服務(wù)都提供了可以用來實現(xiàn)分布式鎖的方法,比如Tair的put方法,redis的setnx方法等。并且,這些緩存服務(wù)也都提供了對數(shù)據(jù)的過期自動刪除的支持,可以直接設(shè)置超時時間來控制鎖的釋放。
使用緩存實現(xiàn)分布式鎖的優(yōu)點:
1、性能好,實現(xiàn)起來較為方便。
使用緩存實現(xiàn)分布式鎖的缺點:
1、通過超時時間來控制鎖的失效時間并不是十分的靠譜。
3、?基于Zookeeper實現(xiàn)分布式鎖
讓我們來回顧一下Zookeeper節(jié)點的概念:
Zookeeper的數(shù)據(jù)存儲結(jié)構(gòu)就像一棵樹,這棵樹由節(jié)點組成,這種節(jié)點叫做Znode。
Znode分為四種類型:
1.持久節(jié)點 (PERSISTENT)
默認(rèn)的節(jié)點類型。創(chuàng)建節(jié)點的客戶端與zookeeper斷開連接后,該節(jié)點依舊存在 。
2.持久節(jié)點順序節(jié)點(PERSISTENT_SEQUENTIAL)
所謂順序節(jié)點,就是在創(chuàng)建節(jié)點時,Zookeeper根據(jù)創(chuàng)建的時間順序給該節(jié)點名稱進(jìn)行編號:
3.臨時節(jié)點(EPHEMERAL)?
和持久節(jié)點相反,當(dāng)創(chuàng)建節(jié)點的客戶端與zookeeper斷開連接后,臨時節(jié)點會被刪除:
4.臨時順序節(jié)點(EPHEMERAL_SEQUENTIAL)?
顧名思義,臨時順序節(jié)點結(jié)合和臨時節(jié)點和順序節(jié)點的特點:在創(chuàng)建節(jié)點時,Zookeeper根據(jù)創(chuàng)建的時間順序給該節(jié)點名稱進(jìn)行編號;當(dāng)創(chuàng)建節(jié)點的客戶端與zookeeper斷開連接后,臨時節(jié)點會被刪除。
Zookeeper分布式鎖的原理
Zookeeper分布式鎖恰恰應(yīng)用了臨時順序節(jié)點。具體如何實現(xiàn)呢?讓我們來看一看詳細(xì)步驟:
獲取鎖
首先,在Zookeeper當(dāng)中創(chuàng)建一個持久節(jié)點ParentLock。當(dāng)?shù)谝粋€客戶端想要獲得鎖時,需要在ParentLock這個節(jié)點下面創(chuàng)建一個臨時順序節(jié)點?Lock1。
之后,Client1查找ParentLock下面所有的臨時順序節(jié)點并排序,判斷自己所創(chuàng)建的節(jié)點Lock1是不是順序最靠前的一個。如果是第一個節(jié)點,則成功獲得鎖。
這時候,如果再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再創(chuàng)建一個臨時順序節(jié)點Lock2。
Client2查找ParentLock下面所有的臨時順序節(jié)點并排序,判斷自己所創(chuàng)建的節(jié)點Lock2是不是順序最靠前的一個,結(jié)果發(fā)現(xiàn)節(jié)點Lock2并不是最小的。
于是,Client2向排序僅比它靠前的節(jié)點Lock1注冊Watcher,用于監(jiān)聽Lock1節(jié)點是否存在。這意味著Client2搶鎖失敗,進(jìn)入了等待狀態(tài)。
這時候,如果又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再創(chuàng)建一個臨時順序節(jié)點Lock3。
Client3查找ParentLock下面所有的臨時順序節(jié)點并排序,判斷自己所創(chuàng)建的節(jié)點Lock3是不是順序最靠前的一個,結(jié)果同樣發(fā)現(xiàn)節(jié)點Lock3并不是最小的。
于是,Client3向排序僅比它靠前的節(jié)點Lock2注冊Watcher,用于監(jiān)聽Lock2節(jié)點是否存在。這意味著Client3同樣搶鎖失敗,進(jìn)入了等待狀態(tài)。
這樣一來,Client1得到了鎖,Client2監(jiān)聽了Lock1,Client3監(jiān)聽了Lock2。這恰恰形成了一個等待隊列,很像是Java當(dāng)中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)。
那么,zookeeper如何釋放鎖呢?
釋放鎖的過程很簡單:只需要刪除對應(yīng)的子節(jié)點就好了。
釋放鎖
釋放鎖分為兩種情況:
1.任務(wù)完成,客戶端顯式釋放
當(dāng)任務(wù)完成時,Client1會顯示調(diào)用刪除節(jié)點Lock1的指令。
2.任務(wù)執(zhí)行過程中,客戶端崩潰
獲得鎖的Client1在任務(wù)執(zhí)行過程中,如果Duang的一聲崩潰,則會斷開與Zookeeper服務(wù)端的鏈接。根據(jù)臨時節(jié)點的特性,相關(guān)聯(lián)的節(jié)點Lock1會隨之自動刪除。
由于Client2一直監(jiān)聽著Lock1的存在狀態(tài),當(dāng)Lock1節(jié)點被刪除,Client2會立刻收到通知。這時候Client2會再次查詢ParentLock下面的所有節(jié)點,確認(rèn)自己創(chuàng)建的節(jié)點Lock2是不是目前最小的節(jié)點。如果是最小,則Client2順理成章獲得了鎖。
同理,如果Client2也因為任務(wù)完成或者節(jié)點崩潰而刪除了節(jié)點Lock2,那么Client3就會接到通知。
Zookeeper和Redis分布式鎖的比較
下面的表格總結(jié)了Zookeeper和Redis分布式鎖的優(yōu)缺點:
有人說Zookeeper實現(xiàn)的分布式鎖支持可重入,Redis實現(xiàn)的分布式鎖不支持可重入,這是錯誤的觀點。兩者都可以在客戶端實現(xiàn)可重入邏輯。
在Apache的開源框架?Apache Curator?中,包含了對Zookeeper分布式鎖的實現(xiàn),有興趣的小伙伴可以看看源碼:
https://github.com/apache/curator/
使用ZK實現(xiàn)的分布式鎖好像完全符合了本文開頭我們對一個分布式鎖的所有期望。但是,其實并不是,Zookeeper實現(xiàn)的分布式鎖其實存在一個缺點,那就是性能上可能并沒有緩存服務(wù)那么高。因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀臨時節(jié)點來實現(xiàn)鎖功能。ZK中創(chuàng)建和刪除節(jié)點只能通過Leader服務(wù)器來執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機(jī)器上。
其實,使用Zookeeper也有可能帶來并發(fā)問題,只是并不常見而已。考慮這樣的情況,由于網(wǎng)絡(luò)抖動,客戶端和ZK集群的session連接斷了,那么zk以為客戶端掛了,就會刪除臨時節(jié)點,這時候其他客戶端就可以獲取到分布式鎖了。就可能產(chǎn)生并發(fā)問題。這個問題不常見是因為zk有重試機(jī)制,一旦zk集群檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節(jié)點。(所以,選擇一個合適的重試策略也比較重要,要在鎖的粒度和并發(fā)之間找一個平衡。)
總結(jié)
使用Zookeeper實現(xiàn)分布式鎖的優(yōu)點:
1、有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現(xiàn)起來較為簡單。
使用Zookeeper實現(xiàn)分布式鎖的缺點:
2、性能上不如使用緩存實現(xiàn)分布式鎖。 需要對ZK的原理有所了解。
三種方案的比較
上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在復(fù)雜性、可靠性、性能等方面無法同時滿足,所以,根據(jù)不同的應(yīng)用場景選擇最適合自己的才是王道。
從理解的難易程度角度(從低到高)
數(shù)據(jù)庫 > 緩存 > Zookeeper
從實現(xiàn)的復(fù)雜性角度(從低到高)
Zookeeper >= 緩存 > 數(shù)據(jù)庫
從性能角度(從高到低)
緩存 > Zookeeper >= 數(shù)據(jù)庫
從可靠性角度(從高到低)
Zookeeper > 緩存 > 數(shù)據(jù)庫
背景知識
目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統(tǒng)在設(shè)計之初就要對這三者做出取舍。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景中,通常都是犧牲強(qiáng)一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可。