JAVA分布式鎖的原理及實(shí)現(xiàn)

引題

比如在同一個(gè)節(jié)點(diǎn)上,兩個(gè)線程并發(fā)的操作A的賬戶苦银,都是取錢贝椿,如果不加鎖烙博,A的賬戶可能會(huì)出現(xiàn)負(fù)數(shù)瑟蜈,正確的方式是對(duì)賬戶acount進(jìn)行加鎖,即使用synchronized關(guān)鍵字习勤,對(duì)其進(jìn)行加鎖后踪栋,當(dāng)有線程訪問(wèn)時(shí),會(huì)獲得鎖图毕,并對(duì)其資源進(jìn)行修改操作夷都,其他的線程只有當(dāng)該線程修改完成后并且釋放鎖,才能對(duì)其訪問(wèn),這種加鎖--修改--釋放鎖的模式就解決了多個(gè)線程同時(shí)修改資源而造成的錯(cuò)誤囤官。

但是冬阳,在分布式集群系統(tǒng)中,兩個(gè)節(jié)點(diǎn)上的兩個(gè)進(jìn)程需要執(zhí)行同一段代碼塊党饮,訪問(wèn)同一個(gè)臨界資源肝陪,如何保證不同節(jié)點(diǎn)的進(jìn)程同步執(zhí)行呢?


一刑顺、什么是分布式鎖(What)

什么是鎖

在單進(jìn)程的系統(tǒng)中氯窍,當(dāng)存在多個(gè)線程可以同時(shí)改變某個(gè)變量(可變共享變量)時(shí),就需要對(duì)變量或代碼塊做同步蹲堂,使其在修改這種變量時(shí)能夠線性執(zhí)行消除并發(fā)修改變量狼讨。

而同步的本質(zhì)是通過(guò)鎖來(lái)實(shí)現(xiàn)的。為了實(shí)現(xiàn)多個(gè)線程在一個(gè)時(shí)刻同一個(gè)代碼塊只能有一個(gè)線程可執(zhí)行柒竞,那么需要在某個(gè)地方做個(gè)標(biāo)記政供,這個(gè)標(biāo)記必須每個(gè)線程都能看到,當(dāng)標(biāo)記不存在時(shí)可以設(shè)置該標(biāo)記朽基,其余后續(xù)線程發(fā)現(xiàn)已經(jīng)有標(biāo)記了則等待擁有標(biāo)記的線程結(jié)束同步代碼塊取消標(biāo)記后再去嘗試設(shè)置標(biāo)記布隔。這個(gè)標(biāo)記可以理解為鎖。

不同地方實(shí)現(xiàn)鎖的方式也不一樣稼虎,只要能滿足所有線程都能看得到標(biāo)記即可衅檀。如java中synchronize是在對(duì)象頭設(shè)置標(biāo)記,Lock接口的實(shí)現(xiàn)類基本上都只是某一個(gè)volitile修飾的int型變量其保證每個(gè)線程都能擁有對(duì)該int的可見(jiàn)性和原子修改渡蜻,linux內(nèi)核中也是利用互斥量或信號(hào)量等內(nèi)存數(shù)據(jù)做標(biāo)記术吝。


分布式情況

此處主要指集群模式下,多個(gè)相同服務(wù)同時(shí)開(kāi)啟.

分布式與單機(jī)情況下最大的不同在于其不是多線程而是多進(jìn)程茸苇。

多線程由于可以共享堆內(nèi)存排苍,因此可以簡(jiǎn)單的采取內(nèi)存作為標(biāo)記存儲(chǔ)位置。而進(jìn)程之間甚至可能都不在同一臺(tái)物理機(jī)上学密,因此需要將標(biāo)記存儲(chǔ)在一個(gè)所有進(jìn)程都能看到的地方淘衙。

分布式鎖

當(dāng)在分布式模型下,數(shù)據(jù)可能只有一份腻暮,此時(shí)需要利用鎖的技術(shù)控制某一時(shí)刻修改數(shù)據(jù)的進(jìn)程數(shù)彤守。

與單機(jī)模式下的鎖不同,分布式鎖不僅需要保證進(jìn)程可見(jiàn)哭靖,還需要考慮進(jìn)程與鎖之間的網(wǎng)絡(luò)問(wèn)題具垫。(分布式情況下之所以問(wèn)題變得復(fù)雜,主要就是需要考慮到網(wǎng)絡(luò)的延時(shí)和不可靠)

分布式鎖還是可以將標(biāo)記存在內(nèi)存试幽,只是該內(nèi)存不是某個(gè)進(jìn)程分配的內(nèi)存而是公共內(nèi)存如Redis筝蚕、Memcache。至于利用數(shù)據(jù)庫(kù)、文件等做鎖與單機(jī)的實(shí)現(xiàn)是一樣的起宽,只要保證標(biāo)記能互斥就行洲胖。


二、為什么需要分布式鎖(Why)

哪些場(chǎng)景需要用

場(chǎng)景一:比較敏感的數(shù)據(jù)比如金額修改坯沪,同一時(shí)間只能有一個(gè)人操作绿映,想象下2個(gè)人同時(shí)修改金額,一個(gè)加金額一個(gè)減金額腐晾,為了防止同時(shí)操作造成數(shù)據(jù)不一致叉弦,需要鎖,如果是數(shù)據(jù)庫(kù)需要的就是行鎖或表鎖藻糖,如果是在集群里卸奉,多個(gè)客戶端同時(shí)修改一個(gè)共享的數(shù)據(jù)就需要分布式鎖。

場(chǎng)景二:比如多臺(tái)機(jī)器都可以定時(shí)執(zhí)行某個(gè)任務(wù)颖御,如果限制任務(wù)每次只能被一臺(tái)機(jī)器執(zhí)行,不能重復(fù)執(zhí)行凝颇,就可以用分布式鎖來(lái)做標(biāo)記潘拱。

場(chǎng)景三:比如秒殺場(chǎng)景,要求并發(fā)量很高拧略,那么同一件商品只能被一個(gè)用戶搶到芦岂,那么就可以使用分布式鎖實(shí)現(xiàn)。


三垫蛆、分布式鎖的幾種實(shí)現(xiàn)方式(How)

針對(duì)分布式鎖的實(shí)現(xiàn)禽最,目前比較常用的有以下幾種方案:

1、基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖?

2袱饭、基于緩存(redis川无,memcached,tair)實(shí)現(xiàn)分布式鎖

?3虑乖、基于Zookeeper實(shí)現(xiàn)分布式鎖

在分析這幾種實(shí)現(xiàn)方案之前我們先來(lái)想一下懦趋,我們需要的分布式鎖應(yīng)該是怎么樣的?(這里以方法鎖為例疹味,資源鎖同理)

可以保證在分布式部署的應(yīng)用集群中仅叫,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)進(jìn)程執(zhí)行。

A 這把鎖要是一把可重入鎖(避免死鎖)

B 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)

C 有高可用的獲取鎖和釋放鎖功能

D 獲取鎖和釋放鎖的性能要好


1糙捺、基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖

基于數(shù)據(jù)庫(kù)表

要實(shí)現(xiàn)分布式鎖诫咱,最簡(jiǎn)單的方式可能就是直接創(chuàng)建一張鎖表,然后通過(guò)操作該表中的數(shù)據(jù)來(lái)實(shí)現(xiàn)了洪灯。

當(dāng)我們要鎖住某個(gè)方法或資源時(shí)坎缭,我們就在該表中增加一條記錄,想要釋放鎖的時(shí)候就刪除這條記錄。

創(chuàng)建這樣一張數(shù)據(jù)庫(kù)表:

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ù)時(shí)間幻锁,自動(dòng)生成',

PRIMARYKEY(`id`),

UNIQUEKEY `uidx_method_name` (`method_name `)USING BTREE

)ENGINE=InnoDB DEFAULTCHARSET=utf8 COMMENT='鎖定中的方法';

當(dāng)我們想要鎖住某個(gè)方法時(shí)凯亮,執(zhí)行以下SQL:

insert into methodLock(method_name ,desc) values(‘method_name’ ,‘desc’)

因?yàn)槲覀儗?duì)method_name做了唯一性約束,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話哄尔,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功假消,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容岭接。

當(dāng)方法執(zhí)行完畢之后富拗,想要釋放鎖的話,需要執(zhí)行以下Sql:

delete from methodLock where method_name ='method_name'

上面這種簡(jiǎn)單的實(shí)現(xiàn)有以下幾個(gè)問(wèn)題:

1鸣戴、這把鎖強(qiáng)依賴數(shù)據(jù)庫(kù)的可用性啃沪,數(shù)據(jù)庫(kù)是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫(kù)掛掉窄锅,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用创千。

2、這把鎖沒(méi)有失效時(shí)間入偷,一旦解鎖操作失敗追驴,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中,其他線程無(wú)法再獲得到鎖疏之。

3殿雪、這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作锋爪,一旦插入失敗就會(huì)直接報(bào)錯(cuò)丙曙。沒(méi)有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作其骄。

4亏镰、這把鎖是非重入的,同一個(gè)線程在沒(méi)有釋放鎖之前無(wú)法再次獲得該鎖年栓。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了拆挥。

當(dāng)然,我們也可以有其他方式解決上面的問(wèn)題某抓。

數(shù)據(jù)庫(kù)是單點(diǎn)纸兔?搞兩個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)之前雙向同步否副。一旦掛掉快速切換到備庫(kù)上汉矿。

沒(méi)有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù)备禀,每隔一定時(shí)間把數(shù)據(jù)庫(kù)中的超時(shí)數(shù)據(jù)清理一遍洲拇。

非阻塞的奈揍?搞一個(gè)while循環(huán),直到insert成功再返回成功赋续。

非重入的男翰?在數(shù)據(jù)庫(kù)表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息纽乱,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫(kù)蛾绎,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫(kù)可以查到的話,直接把鎖分配給他就可以了鸦列。


基于數(shù)據(jù)庫(kù)排他鎖

除了可以通過(guò)增刪操作數(shù)據(jù)表中的記錄以外租冠,其實(shí)還可以借助數(shù)據(jù)庫(kù)自帶的鎖來(lái)實(shí)現(xiàn)分布式的鎖。

我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫(kù)表薯嗤⊥绲可以通過(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。 基于MySql的InnoDB引擎骆姐,可以使用以下方法來(lái)實(shí)現(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;

}

在查詢語(yǔ)句后面增加for update索守,數(shù)據(jù)庫(kù)會(huì)在查詢過(guò)程中給數(shù)據(jù)庫(kù)表增加排他鎖(這里再多提一句搀矫,InnoDB引擎在加鎖的時(shí)候缝裤,只有通過(guò)索引進(jìn)行檢索的時(shí)候才會(huì)使用行級(jí)鎖惑折,否則會(huì)使用表級(jí)鎖。這里我們希望使用行級(jí)鎖归园,就要給method_name添加索引,值得注意的是稚矿,這個(gè)索引一定要?jiǎng)?chuàng)建成唯一索引庸诱,否則會(huì)出現(xiàn)多個(gè)重載方法之間無(wú)法同時(shí)被訪問(wèn)的問(wèn)題。重載方法的話建議把參數(shù)類型也加上晤揣。)桥爽。當(dāng)某條記錄被加上排他鎖之后,其他線程無(wú)法再在該行記錄上增加排他鎖昧识。

我們可以認(rèn)為獲得排它鎖的線程即可獲得分布式鎖钠四,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯跪楞,執(zhí)行完方法之后缀去,再通過(guò)以下方法解鎖:

public void unlock(){

????connection.commit();

}

通過(guò)connection.commit()操作來(lái)釋放鎖。

這種方法可以有效的解決上面提到的無(wú)法釋放鎖和阻塞鎖的問(wèn)題甸祭。

阻塞鎖缕碎?for update語(yǔ)句會(huì)在執(zhí)行成功后立即返回,在執(zhí)行失敗時(shí)一直處于阻塞狀態(tài)池户,直到成功咏雌。

鎖定之后服務(wù)宕機(jī)凡怎,無(wú)法釋放?使用這種方式赊抖,服務(wù)宕機(jī)之后數(shù)據(jù)庫(kù)會(huì)自己把鎖釋放掉统倒。

但是還是無(wú)法解決數(shù)據(jù)庫(kù)單點(diǎn)和可重入問(wèn)題。

這種方式還有一個(gè)問(wèn)題氛雪,就是我們要使用排他鎖來(lái)進(jìn)行分布式鎖的lock房匆,那么一個(gè)排他鎖長(zhǎng)時(shí)間不提交,就會(huì)占用數(shù)據(jù)庫(kù)連接注暗。一旦類似的連接變得多了坛缕,就可能把數(shù)據(jù)庫(kù)連接池?fù)伪?/p>


總結(jié)

總結(jié)一下使用數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)分布式鎖的方式,這兩種方式都是依賴數(shù)據(jù)庫(kù)的一張表捆昏,一種是通過(guò)表中的記錄的存在情況確定當(dāng)前是否有鎖存在赚楚,另外一種是通過(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn):

1骗卜、直接借助數(shù)據(jù)庫(kù)宠页,容易理解。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的缺點(diǎn):

1寇仓、會(huì)有各種各樣的問(wèn)題举户,在解決問(wèn)題的過(guò)程中會(huì)使整個(gè)方案變得越來(lái)越復(fù)雜。

2遍烦、操作數(shù)據(jù)庫(kù)需要一定的開(kāi)銷俭嘁,性能問(wèn)題需要考慮。

3服猪、使用數(shù)據(jù)庫(kù)的行級(jí)鎖并不一定靠譜供填,尤其是當(dāng)我們的鎖表并不大的時(shí)候。


2?基于緩存實(shí)現(xiàn)分布式鎖

相比較于基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的方案來(lái)說(shuō)罢猪,基于緩存來(lái)實(shí)現(xiàn)在性能方面會(huì)表現(xiàn)的更好一點(diǎn)近她。而且很多緩存是可以集群部署的,可以解決單點(diǎn)問(wèn)題膳帕。

目前有很多成熟的緩存產(chǎn)品粘捎,包括Redis,memcached以及阿里內(nèi)部用的Tair危彩。

這里以Redis為例:

Redis分布式鎖的基本流程并不難理解攒磨,但要想寫(xiě)得盡善盡美,也并不是那么容易汤徽。在這里咧纠,我們需要先了解分布式鎖實(shí)現(xiàn)的三個(gè)核心要素:

1.加鎖

最簡(jiǎn)單的方法是使用setnx命令。key是鎖的唯一標(biāo)識(shí)泻骤,按業(yè)務(wù)來(lái)決定命名漆羔。比如想要給一種商品的秒殺活動(dòng)加鎖梧奢,可以給key命名為 “l(fā)ock_sale_商品ID” 。而value設(shè)置成什么呢演痒?我們可以姑且設(shè)置成1亲轨。加鎖的偽代碼如下:

setnx(key,1)

當(dāng)一個(gè)線程執(zhí)行setnx返回1鸟顺,說(shuō)明key原本不存在惦蚊,該線程成功得到了鎖;當(dāng)一個(gè)線程執(zhí)行setnx返回0讯嫂,說(shuō)明key已經(jīng)存在蹦锋,該線程搶鎖失敗。

2.解鎖

有加鎖就得有解鎖欧芽。當(dāng)?shù)玫芥i的線程執(zhí)行完任務(wù)莉掂,需要釋放鎖,以便其他線程可以進(jìn)入千扔。釋放鎖的最簡(jiǎn)單方式是執(zhí)行del指令憎妙,偽代碼如下:

del(key)

釋放鎖之后,其他線程就可以繼續(xù)執(zhí)行setnx命令來(lái)獲得鎖曲楚。

3.鎖超時(shí)

鎖超時(shí)是什么意思呢厘唾?如果一個(gè)得到鎖的線程在執(zhí)行任務(wù)的過(guò)程中掛掉,來(lái)不及顯式地釋放鎖龙誊,這塊資源將會(huì)永遠(yuǎn)被鎖住抚垃,別的線程再也別想進(jìn)來(lái)。

所以趟大,setnx的key必須設(shè)置一個(gè)超時(shí)時(shí)間讯柔,以保證即使沒(méi)有被顯式釋放,這把鎖也要在一定時(shí)間后自動(dòng)釋放护昧。setnx不支持超時(shí)參數(shù),所以需要額外的指令粗截,偽代碼如下:

expire(key惋耙, 30)

綜合起來(lái),我們分布式鎖實(shí)現(xiàn)的第一版?zhèn)未a如下:

if(setnx(key熊昌,1) == 1){? ??

????????expire(key绽榛,30)? ??

????????try {? ? ? ??

????????????do something ......? ??

????????} finally {? ? ? ??

????????????del(key)? ? }

}


上面的偽代碼中,存在著三個(gè)致命問(wèn)題:

1婿屹、setnx和expire的非原子性

設(shè)想一個(gè)極端場(chǎng)景灭美,當(dāng)某線程執(zhí)行setnx,成功得到了鎖:


setnx剛執(zhí)行成功昂利,還未來(lái)得及執(zhí)行expire指令届腐,節(jié)點(diǎn)1 突然掛掉了铁坎。

這樣一來(lái),這把鎖就沒(méi)有設(shè)置過(guò)期時(shí)間犁苏,變得“長(zhǎng)生不老”硬萍,別的線程再也無(wú)法獲得鎖了。

怎么解決呢围详?setnx指令本身是不支持傳入超時(shí)時(shí)間的朴乖,幸好Redis 2.6.12以上版本為set指令增加了可選參數(shù),偽代碼如下:

set(key助赞,1买羞,30,NX)

這樣就可以取代setnx指令雹食。

2畜普、del 導(dǎo)致誤刪

又是一個(gè)極端場(chǎng)景,假如某線程成功得到了鎖婉徘,并且設(shè)置的超時(shí)時(shí)間是30秒漠嵌。

如果某些原因?qū)е戮€程A執(zhí)行的很慢很慢,過(guò)了30秒都沒(méi)執(zhí)行完盖呼,這時(shí)候鎖過(guò)期自動(dòng)釋放儒鹿,線程B得到了鎖。


隨后几晤,線程A執(zhí)行完了任務(wù)约炎,線程A接著執(zhí)行del指令來(lái)釋放鎖。但這時(shí)候線程B還沒(méi)執(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è)新的問(wèn)題编兄,判斷和釋放鎖是兩個(gè)獨(dú)立操作轩性,不是原子性。

我們都是追求極致的程序員狠鸳,所以這一塊要用Lua腳本來(lái)實(shí)現(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));

這樣一來(lái)揣苏,驗(yàn)證和刪除過(guò)程就是原子操作了悯嗓。

3.、出現(xiàn)并發(fā)的可能性

還是剛才第二點(diǎn)所描述的場(chǎng)景舒岸,雖然我們避免了線程A誤刪掉key的情況绅作,但是同一時(shí)間有A,B兩個(gè)線程在訪問(wèn)代碼塊蛾派,相當(dāng)于鎖的作用失效了俄认。

怎么辦呢?我們可以讓獲得鎖的線程開(kāi)啟一個(gè)守護(hù)線程洪乍,用來(lái)給快要過(guò)期的鎖“續(xù)命”眯杏。

當(dāng)過(guò)去了29秒,線程A還沒(méi)執(zhí)行完壳澳,這時(shí)候守護(hù)線程會(huì)執(zhí)行expire指令岂贩,為這把鎖“續(xù)命”20秒。守護(hù)線程從第29秒開(kāi)始執(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í)候,沒(méi)人給它續(xù)命终佛,也就自動(dòng)釋放了俊嗽。

總結(jié)

可以使用緩存來(lái)代替數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)分布式鎖,這個(gè)可以提供更好的性能铃彰,同時(shí)绍豁,很多緩存服務(wù)都是集群部署的,可以避免單點(diǎn)問(wèn)題牙捉。并且很多緩存服務(wù)都提供了可以用來(lái)實(shí)現(xiàn)分布式鎖的方法竹揍,比如Tair的put方法,redis的setnx方法等鹃共。并且,這些緩存服務(wù)也都提供了對(duì)數(shù)據(jù)的過(guò)期自動(dòng)刪除的支持驶拱,可以直接設(shè)置超時(shí)時(shí)間來(lái)控制鎖的釋放霜浴。

使用緩存實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn):

1、性能好蓝纲,實(shí)現(xiàn)起來(lái)較為方便阴孟。

使用緩存實(shí)現(xiàn)分布式鎖的缺點(diǎn):

1晌纫、通過(guò)超時(shí)時(shí)間來(lái)控制鎖的失效時(shí)間并不是十分的靠譜。


3永丝、?基于Zookeeper實(shí)現(xiàn)分布式鎖

讓我們來(lái)回顧一下Zookeeper節(jié)點(diǎn)的概念:



Zookeeper的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)就像一棵樹(shù)锹漱,這棵樹(shù)由節(jié)點(diǎn)組成,這種節(jié)點(diǎn)叫做Znode慕嚷。

Znode分為四種類型:

1.持久節(jié)點(diǎn) (PERSISTENT)

默認(rèn)的節(jié)點(diǎn)類型哥牍。創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開(kāi)連接后,該節(jié)點(diǎn)依舊存在 喝检。

2.持久節(jié)點(diǎn)順序節(jié)點(diǎn)(PERSISTENT_SEQUENTIAL)

所謂順序節(jié)點(diǎn)嗅辣,就是在創(chuàng)建節(jié)點(diǎn)時(shí),Zookeeper根據(jù)創(chuàng)建的時(shí)間順序給該節(jié)點(diǎn)名稱進(jìn)行編號(hào):


3.臨時(shí)節(jié)點(diǎn)(EPHEMERAL)?

和持久節(jié)點(diǎn)相反挠说,當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開(kāi)連接后澡谭,臨時(shí)節(jié)點(diǎn)會(huì)被刪除:




4.臨時(shí)順序節(jié)點(diǎn)(EPHEMERAL_SEQUENTIAL)?

顧名思義,臨時(shí)順序節(jié)點(diǎn)結(jié)合和臨時(shí)節(jié)點(diǎn)和順序節(jié)點(diǎn)的特點(diǎn):在創(chuàng)建節(jié)點(diǎn)時(shí)损俭,Zookeeper根據(jù)創(chuàng)建的時(shí)間順序給該節(jié)點(diǎn)名稱進(jìn)行編號(hào)蛙奖;當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開(kāi)連接后,臨時(shí)節(jié)點(diǎn)會(huì)被刪除杆兵。

Zookeeper分布式鎖的原理

Zookeeper分布式鎖恰恰應(yīng)用了臨時(shí)順序節(jié)點(diǎn)雁仲。具體如何實(shí)現(xiàn)呢?讓我們來(lái)看一看詳細(xì)步驟:

獲取鎖

首先拧咳,在Zookeeper當(dāng)中創(chuàng)建一個(gè)持久節(jié)點(diǎn)ParentLock伯顶。當(dāng)?shù)谝粋€(gè)客戶端想要獲得鎖時(shí),需要在ParentLock這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)?Lock1骆膝。


之后祭衩,Client1查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock1是不是順序最靠前的一個(gè)阅签。如果是第一個(gè)節(jié)點(diǎn)掐暮,則成功獲得鎖。


這時(shí)候政钟,如果再有一個(gè)客戶端 Client2 前來(lái)獲取鎖路克,則在ParentLock下載再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock2。


Client2查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序养交,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock2是不是順序最靠前的一個(gè)精算,結(jié)果發(fā)現(xiàn)節(jié)點(diǎn)Lock2并不是最小的。

于是碎连,Client2向排序僅比它靠前的節(jié)點(diǎn)Lock1注冊(cè)Watcher灰羽,用于監(jiān)聽(tīng)Lock1節(jié)點(diǎn)是否存在。這意味著Client2搶鎖失敗,進(jìn)入了等待狀態(tài)廉嚼。



這時(shí)候玫镐,如果又有一個(gè)客戶端Client3前來(lái)獲取鎖,則在ParentLock下載再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock3怠噪。


Client3查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序恐似,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock3是不是順序最靠前的一個(gè),結(jié)果同樣發(fā)現(xiàn)節(jié)點(diǎn)Lock3并不是最小的傍念。

于是矫夷,Client3向排序僅比它靠前的節(jié)點(diǎn)Lock2注冊(cè)Watcher,用于監(jiān)聽(tīng)Lock2節(jié)點(diǎn)是否存在捂寿。這意味著Client3同樣搶鎖失敗口四,進(jìn)入了等待狀態(tài)。



這樣一來(lái)秦陋,Client1得到了鎖蔓彩,Client2監(jiān)聽(tīng)了Lock1,Client3監(jiān)聽(tīng)了Lock2驳概。這恰恰形成了一個(gè)等待隊(duì)列赤嚼,很像是Java當(dāng)中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)。

那么,zookeeper如何釋放鎖呢顺又?

釋放鎖的過(guò)程很簡(jiǎn)單:只需要?jiǎng)h除對(duì)應(yīng)的子節(jié)點(diǎn)就好了更卒。

釋放鎖

釋放鎖分為兩種情況:

1.任務(wù)完成,客戶端顯式釋放

當(dāng)任務(wù)完成時(shí)稚照,Client1會(huì)顯示調(diào)用刪除節(jié)點(diǎn)Lock1的指令蹂空。


2.任務(wù)執(zhí)行過(guò)程中,客戶端崩潰

獲得鎖的Client1在任務(wù)執(zhí)行過(guò)程中果录,如果Duang的一聲崩潰上枕,則會(huì)斷開(kāi)與Zookeeper服務(wù)端的鏈接。根據(jù)臨時(shí)節(jié)點(diǎn)的特性弱恒,相關(guān)聯(lián)的節(jié)點(diǎn)Lock1會(huì)隨之自動(dòng)刪除辨萍。


由于Client2一直監(jiān)聽(tīng)著Lock1的存在狀態(tài),當(dāng)Lock1節(jié)點(diǎn)被刪除返弹,Client2會(huì)立刻收到通知锈玉。這時(shí)候Client2會(huì)再次查詢ParentLock下面的所有節(jié)點(diǎn),確認(rèn)自己創(chuàng)建的節(jié)點(diǎn)Lock2是不是目前最小的節(jié)點(diǎn)义起。如果是最小拉背,則Client2順理成章獲得了鎖。

同理默终,如果Client2也因?yàn)槿蝿?wù)完成或者節(jié)點(diǎn)崩潰而刪除了節(jié)點(diǎn)Lock2椅棺,那么Client3就會(huì)接到通知抡诞。

Zookeeper和Redis分布式鎖的比較

下面的表格總結(jié)了Zookeeper和Redis分布式鎖的優(yōu)缺點(diǎn):


有人說(shuō)Zookeeper實(shí)現(xiàn)的分布式鎖支持可重入,Redis實(shí)現(xiàn)的分布式鎖不支持可重入土陪,這是錯(cuò)誤的觀點(diǎn)。兩者都可以在客戶端實(shí)現(xiàn)可重入邏輯肴熏。

在Apache的開(kāi)源框架?Apache Curator?中鬼雀,包含了對(duì)Zookeeper分布式鎖的實(shí)現(xiàn),有興趣的小伙伴可以看看源碼:

https://github.com/apache/curator/

使用ZK實(shí)現(xiàn)的分布式鎖好像完全符合了本文開(kāi)頭我們對(duì)一個(gè)分布式鎖的所有期望蛙吏。但是源哩,其實(shí)并不是,Zookeeper實(shí)現(xiàn)的分布式鎖其實(shí)存在一個(gè)缺點(diǎn)鸦做,那就是性能上可能并沒(méi)有緩存服務(wù)那么高励烦。因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過(guò)程中,都要?jiǎng)討B(tài)創(chuàng)建泼诱、銷毀臨時(shí)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖功能坛掠。ZK中創(chuàng)建和刪除節(jié)點(diǎn)只能通過(guò)Leader服務(wù)器來(lái)執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機(jī)器上治筒。

其實(shí)屉栓,使用Zookeeper也有可能帶來(lái)并發(fā)問(wèn)題,只是并不常見(jiàn)而已耸袜∮讯啵考慮這樣的情況,由于網(wǎng)絡(luò)抖動(dòng)堤框,客戶端和ZK集群的session連接斷了域滥,那么zk以為客戶端掛了,就會(huì)刪除臨時(shí)節(jié)點(diǎn)蜈抓,這時(shí)候其他客戶端就可以獲取到分布式鎖了启绰。就可能產(chǎn)生并發(fā)問(wèn)題。這個(gè)問(wèn)題不常見(jiàn)是因?yàn)閦k有重試機(jī)制资昧,一旦zk集群檢測(cè)不到客戶端的心跳酬土,就會(huì)重試,Curator客戶端支持多種重試策略格带。多次重試之后還不行的話才會(huì)刪除臨時(shí)節(jié)點(diǎn)撤缴。(所以,選擇一個(gè)合適的重試策略也比較重要叽唱,要在鎖的粒度和并發(fā)之間找一個(gè)平衡屈呕。)

總結(jié)

使用Zookeeper實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn):

1、有效的解決單點(diǎn)問(wèn)題棺亭,不可重入問(wèn)題虎眨,非阻塞問(wèn)題以及鎖無(wú)法釋放的問(wèn)題。實(shí)現(xiàn)起來(lái)較為簡(jiǎn)單。

使用Zookeeper實(shí)現(xiàn)分布式鎖的缺點(diǎn):

2嗽桩、性能上不如使用緩存實(shí)現(xiàn)分布式鎖岳守。 需要對(duì)ZK的原理有所了解。

三種方案的比較

上面幾種方式碌冶,哪種方式都無(wú)法做到完美湿痢。就像CAP一樣,在復(fù)雜性扑庞、可靠性譬重、性能等方面無(wú)法同時(shí)滿足,所以罐氨,根據(jù)不同的應(yīng)用場(chǎng)景選擇最適合自己的才是王道臀规。

從理解的難易程度角度(從低到高)

數(shù)據(jù)庫(kù) > 緩存 > Zookeeper

從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)

Zookeeper >= 緩存 > 數(shù)據(jù)庫(kù)

從性能角度(從高到低)

緩存 > Zookeeper >= 數(shù)據(jù)庫(kù)

從可靠性角度(從高到低)

Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)

背景知識(shí)

目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場(chǎng)景中的數(shù)據(jù)一致性問(wèn)題一直是一個(gè)比較重要的話題栅隐。分布式的CAP理論告訴我們“任何一個(gè)分布式系統(tǒng)都無(wú)法同時(shí)滿足一致性(Consistency)塔嬉、可用性(Availability)和分區(qū)容錯(cuò)性(Partition tolerance),最多只能同時(shí)滿足兩項(xiàng)租悄∫囟簦”所以,很多系統(tǒng)在設(shè)計(jì)之初就要對(duì)這三者做出取舍恰矩。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場(chǎng)景中记盒,通常都是犧牲強(qiáng)一致性來(lái)?yè)Q取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”外傅,只要這個(gè)最終時(shí)間是在用戶可以接受的范圍內(nèi)即可纪吮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市萎胰,隨后出現(xiàn)的幾起案子碾盟,更是在濱河造成了極大的恐慌,老刑警劉巖技竟,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冰肴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡榔组,警方通過(guò)查閱死者的電腦和手機(jī)熙尉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)搓扯,“玉大人检痰,你說(shuō)我怎么就攤上這事∠峭疲” “怎么了铅歼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵公壤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我椎椰,道長(zhǎng)厦幅,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任慨飘,我火速辦了婚禮慨削,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘套媚。我一直安慰自己,他們只是感情好磁椒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布堤瘤。 她就那樣靜靜地躺著,像睡著了一般浆熔。 火紅的嫁衣襯著肌膚如雪本辐。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天医增,我揣著相機(jī)與錄音慎皱,去河邊找鬼。 笑死叶骨,一個(gè)胖子當(dāng)著我的面吹牛茫多,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忽刽,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼天揖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了跪帝?” 一聲冷哼從身側(cè)響起今膊,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伞剑,沒(méi)想到半個(gè)月后斑唬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黎泣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年恕刘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抒倚。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雪营,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衡便,到底是詐尸還是另有隱情献起,我是刑警寧澤洋访,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站谴餐,受9級(jí)特大地震影響姻政,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岂嗓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一汁展、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厌殉,春花似錦食绿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至楼眷,卻和暖如春铲汪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罐柳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工掌腰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人张吉。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓齿梁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親肮蛹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子士飒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,928評(píng)論 2 89
  • 最近工作中遇到需要使用分布式鎖的場(chǎng)景,對(duì)于用戶的主動(dòng)還款和到期扣款操作我們可能有部分邏輯是一樣的蔗崎,但是主動(dòng)還款是用...
    bigfish1129閱讀 622評(píng)論 1 7
  • 和田玉作為時(shí)下最熱門(mén)的玉石珠寶收藏之一缓苛,其市場(chǎng)價(jià)格一路上漲芳撒,隨著市場(chǎng)的熱度趨升,有些商家拿著其他種類的石頭未桥,低價(jià)冒...
    坑蒙卑鄙來(lái)死狗閱讀 1,190評(píng)論 0 0
  • 在公司吃晚飯笔刹,一群同事坐在一起,我有點(diǎn)質(zhì)問(wèn)的口吻問(wèn)我們小組的老大:“為什么我們的app上沒(méi)有推薦冬耿,很多產(chǎn)品上...
    水_水閱讀 460評(píng)論 0 1