DB\redis\zookeeper分布式鎖設(shè)計(jì)

多線程情況下對共享資源的操作需要加鎖牵现,避免數(shù)據(jù)被寫亂,在分布式系統(tǒng)中灶搜,這個(gè)問題也是存在的祟蚀,此時(shí)就需要一個(gè)分布式鎖服務(wù)。常見的分布式鎖實(shí)現(xiàn)一般是基于DB割卖、Redis前酿、zookeeper。下面筆者會按照順序分析下這3種分布式鎖的設(shè)計(jì)與實(shí)現(xiàn)鹏溯,想直接看分布式鎖總結(jié)的小伙伴可直接翻到文檔末尾處罢维。

分布式鎖的實(shí)現(xiàn)由多種方式,但是不管怎樣丙挽,分布式鎖一般要有以下特點(diǎn):

?排他性:任意時(shí)刻肺孵,只能有一個(gè)client能獲取到鎖

?容錯(cuò)性:分布式鎖服務(wù)一般要滿足AP,也就是說取试,只要分布式鎖服務(wù)集群節(jié)點(diǎn)大部分存活悬槽,client就可以進(jìn)行加鎖解鎖操作

?避免死鎖:分布式鎖一定能得到釋放怀吻,即使client在釋放之前崩潰或者網(wǎng)絡(luò)不可達(dá)

除了以上特點(diǎn)之外瞬浓,分布式鎖最好也能滿足可重入、高性能蓬坡、阻塞鎖特性(AQS這種猿棉,能夠及時(shí)從阻塞狀態(tài)喚醒)等,下面就話不多說屑咳,趕緊上車~

DB鎖

在數(shù)據(jù)庫新建一張表用于控制并發(fā)控制萨赁,表結(jié)構(gòu)可以如下所示:

CREATE TABLE `lock_table` (

? `id` int(11) unsigned NOT NULL COMMENT '主鍵',

? `key_id` bigint(20) NOT NULL COMMENT '分布式key',

? `memo` varchar(43) NOT NULL DEFAULT '' COMMENT '可記錄操作內(nèi)容',

? `update_time` datetime NOT NULL COMMENT '更新時(shí)間',

? PRIMARY KEY (`id`,`key_id`),

? UNIQUE KEY `key_id` (`key_id`) USING BTREE

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

key_id作為分布式key用來并發(fā)控制,memo可用來記錄一些操作內(nèi)容(比如memo可用來支持重入特性兆龙,標(biāo)記下當(dāng)前加鎖的client和加鎖次數(shù))杖爽。將key_id設(shè)置為唯一索引,保證了針對同一個(gè)key_id只有一個(gè)加鎖(數(shù)據(jù)插入)能成功紫皇。此時(shí)lock和unlock偽代碼如下:

def lock :

? ? exec sql: insert into lock_table(key_id, memo, update_time) values (key_id, memo, NOW())

? ? if result == true :

? ? ? ? return true

? ? else :

? ? ? ? return false

def unlock :

? ? exec sql: delete from lock_table where key_id = 'key_id' and memo = 'memo'

注意慰安,偽代碼中的lock操作是非阻塞鎖,也就是tryLock聪铺,如果想實(shí)現(xiàn)阻塞(或者阻塞超時(shí))加鎖化焕,只修反復(fù)執(zhí)行l(wèi)ock偽代碼直到加鎖成功為止即可×逄蓿基于DB的分布式鎖其實(shí)有一個(gè)問題撒桨,那就是如果加鎖成功后查刻,client端宕機(jī)或者由于網(wǎng)絡(luò)原因?qū)е聸]有解鎖,那么其他client就無法對該key_id進(jìn)行加鎖并且無法釋放了凤类。為了能夠讓鎖失效穗泵,需要在應(yīng)用層加上定時(shí)任務(wù),去刪除過期還未解鎖的記錄踱蠢,比如刪除2分鐘前未解鎖的偽代碼如下:

def clear_timeout_lock :

? ? exec sql : delete from lock_table where update_time <? ADDTIME(NOW(),'-00:02:00')

因?yàn)閱螌?shí)例DB的TPS一般為幾百火欧,所以基于DB的分布式性能上限一般也是1k以下,一般在并發(fā)量不大的場景下該分布式鎖是滿足需求的茎截,不會出現(xiàn)性能問題苇侵。不過DB作為分布式鎖服務(wù)需要考慮單點(diǎn)問題,對于分布式系統(tǒng)來說是不允許出現(xiàn)單點(diǎn)的企锌,一般通過數(shù)據(jù)庫的同步復(fù)制榆浓,以及使用vip切換Master就能解決這個(gè)問題。

以上DB分布式鎖是通過insert來實(shí)現(xiàn)的撕攒,如果加鎖的數(shù)據(jù)已經(jīng)在數(shù)據(jù)庫中存在陡鹃,那么用select xxx where key_id = xxx for udpate方式來做也是可以的。

Redis鎖

Redis鎖是通過以下命令對資源進(jìn)行加鎖:

set key_id key_value NX PX expireTime

其中抖坪,set nx命令只會在key不存在時(shí)給key進(jìn)行賦值萍鲸,px用來設(shè)置key過期時(shí)間,key_value一般是隨機(jī)值擦俐,用來保證釋放鎖的安全性(釋放時(shí)會判斷是否是之前設(shè)置過的隨機(jī)值脊阴,只有是才釋放鎖)。由于資源設(shè)置了過期時(shí)間蚯瞧,一定時(shí)間后鎖會自動釋放嘿期。

set nx保證并發(fā)加鎖時(shí)只有一個(gè)client能設(shè)置成功(Redis內(nèi)部是單線程,并且數(shù)據(jù)存在內(nèi)存中埋合,也就是說redis內(nèi)部執(zhí)行命令是不會有多線程同步問題的)备徐,此時(shí)的lock/unlock偽代碼如下:

def lock:

? ? if (redis.call('set', KEYS[1], ARGV[1], 'ex', ARGV[2], 'nx')) then

? ? ? return true

? ? end

? ? ? return false

def unlock:

? ? if (redis.call('get', KEYS[1]) == ARGV[1]) then

? ? ? redis.call('del', KEYS[1])

? ? ? return true

? ? end

? ? ? return false

分布式鎖服務(wù)中的一個(gè)問題

如果一個(gè)獲取到鎖的client因?yàn)槟撤N原因?qū)е聸]能及時(shí)釋放鎖,并且redis因?yàn)槌瑫r(shí)釋放了鎖甚颂,另外一個(gè)client獲取到了鎖.

那么如何解決這個(gè)問題呢蜜猾,一種方案是引入鎖續(xù)約機(jī)制,也就是獲取鎖之后振诬,釋放鎖之前蹭睡,會定時(shí)進(jìn)行鎖續(xù)約,比如以鎖超時(shí)時(shí)間的1/3為間隔周期進(jìn)行鎖續(xù)約贷揽。

關(guān)于開源的redis的分布式鎖實(shí)現(xiàn)有很多棠笑,比較出名的有redisson[1]、百度的dlock[2]禽绪,關(guān)于分布式鎖蓖救,筆者也寫了一個(gè)簡易版的分布式鎖redis-lock洪规,主要是增加了鎖續(xù)約和可同時(shí)針對多個(gè)key加鎖的機(jī)制。

對于高可用性循捺,一般可以通過集群或者master-slave來解決斩例,redis鎖優(yōu)勢是性能出色,劣勢就是由于數(shù)據(jù)在內(nèi)存中从橘,一旦緩存服務(wù)宕機(jī)念赶,鎖數(shù)據(jù)就丟失了。像redis自帶復(fù)制功能恰力,可以對數(shù)據(jù)可靠性有一定的保證叉谜,但是由于復(fù)制也是異步完成的,因此依然可能出現(xiàn)master節(jié)點(diǎn)寫入鎖數(shù)據(jù)而未同步到slave節(jié)點(diǎn)的時(shí)候宕機(jī)踩萎,鎖數(shù)據(jù)丟失問題停局。

zookeeper分布式鎖

ZooKeeper是一個(gè)高可用的分布式協(xié)調(diào)服務(wù),由雅虎創(chuàng)建香府,是Google Chubby的開源實(shí)現(xiàn)董栽。ZooKeeper提供了一項(xiàng)基本的服務(wù):分布式鎖服務(wù)。zookeeper重要的3個(gè)特征是:zab協(xié)議企孩、node存儲模型和watcher機(jī)制锭碳。通過zab協(xié)議保證數(shù)據(jù)一致性,zookeeper集群部署保證可用性勿璃,node存儲在內(nèi)存中擒抛,提高了數(shù)據(jù)操作性能,使用watcher機(jī)制蝗柔,實(shí)現(xiàn)了通知機(jī)制(比如加鎖成功的client釋放鎖時(shí)可以通知到其他client)闻葵。

zookeeper node模型支持臨時(shí)節(jié)點(diǎn)特性民泵,即client寫入的數(shù)據(jù)時(shí)臨時(shí)數(shù)據(jù)癣丧,當(dāng)客戶端宕機(jī)時(shí)臨時(shí)數(shù)據(jù)會被刪除,這樣就不需要給鎖增加超時(shí)釋放機(jī)制了栈妆。當(dāng)針對同一個(gè)path并發(fā)多個(gè)創(chuàng)建請求時(shí)胁编,只有一個(gè)client能創(chuàng)建成功,這個(gè)特性用來實(shí)現(xiàn)分布式鎖鳞尔。注意:如果client端沒有宕機(jī)嬉橙,由于網(wǎng)絡(luò)原因?qū)е聑ookeeper服務(wù)與client心跳失敗,那么zookeeper也會把臨時(shí)數(shù)據(jù)給刪除掉的寥假,這時(shí)如果client還在操作共享數(shù)據(jù)市框,是有一定風(fēng)險(xiǎn)的。

基于zookeeper實(shí)現(xiàn)分布式鎖糕韧,相對于基于redis和DB的實(shí)現(xiàn)來說枫振,使用上更容易喻圃,效率與穩(wěn)定性較好。curator封裝了對zookeeper的api操作粪滤,同時(shí)也封裝了一些高級特性斧拍,如:Cache事件監(jiān)聽、選舉杖小、分布式鎖肆汹、分布式計(jì)數(shù)器、分布式Barrier等予权,使用curator進(jìn)行分布式加鎖示例如下:

<!--引入依賴-->

<!--對zookeeper的底層api的一些封裝-->

<dependency>

? ? <groupId>org.apache.curator</groupId>

? ? <artifactId>curator-framework</artifactId>

? ? <version>2.12.0</version>

</dependency>

<!--封裝了一些高級特性昂勉,如:Cache事件監(jiān)聽、選舉扫腺、分布式鎖硼啤、分布式計(jì)數(shù)器、分布式Barrier等-->

<dependency>

? ? <groupId>org.apache.curator</groupId>

? ? <artifactId>curator-recipes</artifactId>

? ? <version>2.12.0</version>

</dependency>

public static void main(String[] args) throws Exception {

? ? String lockPath = "/curator_recipes_lock_path";

? ? CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.193.128:2181")

? ? ? ? ? ? .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();

? ? client.start();

? ? InterProcessMutex lock = new InterProcessMutex(client, lockPath);

? ? Runnable task = () -> {

? ? ? ? try {

? ? ? ? ? ? lock.acquire();

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? System.out.println("zookeeper acquire success: " + Thread.currentThread().getName());

? ? ? ? ? ? ? ? Thread.sleep(1000);

? ? ? ? ? ? } catch (Exception e) {

? ? ? ? ? ? ? ? e.printStackTrace();

? ? ? ? ? ? } finally {

? ? ? ? ? ? ? ? lock.release();

? ? ? ? ? ? }

? ? ? ? } catch (Exception ex) {

? ? ? ? ? ? ex.printStackTrace();

? ? ? ? }

? ? };

? ? ExecutorService executor = Executors.newFixedThreadPool(10);

? ? for (int i = 0; i < 1000; i++) {

? ? ? ? executor.execute(task);

? ? }

? ? LockSupport.park();

}



zookeeper 加鎖釋放鎖流程:

1斧账、首先谴返,在 Zookeeper 當(dāng)中創(chuàng)建一個(gè)持久節(jié)點(diǎn),給它取個(gè)名字名字叫 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)闪萄,則成功獲得鎖梧却。

2、這時(shí)候败去,如果再有一個(gè)客戶端 Client2 前來獲取鎖放航,則在 ParentLock 下再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn) Lock2。

3圆裕、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 注冊 Watcher,用于監(jiān)聽 Lock1 節(jié)點(diǎn)是否存在行拢。這意味著 Client2 搶鎖失敗祖秒,進(jìn)入了等待狀態(tài),這就形成了一個(gè)等待隊(duì)列。

4竭缝、 當(dāng)任務(wù)完成時(shí)狐胎,Client1 會顯示調(diào)用刪除節(jié)點(diǎn) Lock1 的指令。此時(shí)由于 Client2 一直監(jiān)聽著 Lock1 的存在狀態(tài)歌馍,當(dāng) Lock1 節(jié)點(diǎn)被刪除握巢,Client2 會立刻收到通知。這時(shí)候 Client2 會再次查詢 ParentLock 下面的所有節(jié)點(diǎn)松却,確認(rèn)自己創(chuàng)建的節(jié)點(diǎn) Lock2 是不是目前最小的節(jié)點(diǎn)暴浦。如果是最小,則 Client2 順理成章獲得了鎖晓锻。

5歌焦、如果說獲得鎖的 Client1 在任務(wù)執(zhí)行過程中如果崩潰了,則會斷開與 Zookeeper 服務(wù)端的鏈接砚哆。根據(jù)臨時(shí)節(jié)點(diǎn)的特性独撇,相關(guān)聯(lián)的節(jié)點(diǎn) Lock1 會隨之自動刪除;所以Client2 又收到通知獲得了鎖躁锁。

說到這纷铣,是不是覺得Zookeeper的機(jī)制來實(shí)現(xiàn)分布式鎖較為不錯(cuò),人家都給你封裝好了战转,不用向Redis那樣實(shí)現(xiàn)分布式鎖還得考慮超時(shí)搜立、原子性、誤刪除之類的槐秧。

還有等待隊(duì)列可以提升搶鎖效率啄踊,比起Redis實(shí)現(xiàn)的效果確實(shí)是舒服了許多。


總結(jié)

從上面介紹的3種分布式鎖的設(shè)計(jì)與實(shí)現(xiàn)中刁标,我們可以看出每種實(shí)現(xiàn)都有各自的特點(diǎn)颠通,針對潛在的問題有不同的解決方案,歸納如下:

?性能:redis > zookeeper > db膀懈。

?避免死鎖:DB通過應(yīng)用層設(shè)置定時(shí)任務(wù)來刪除過期還未釋放的鎖顿锰,redis通過設(shè)置超時(shí)時(shí)間來解決,而zookeeper是通過臨時(shí)節(jié)點(diǎn)來解決吏砂。

?可用性:DB可通過數(shù)據(jù)庫同步復(fù)制撵儿,vip切換master來解決乘客,redis可通過集群或者master-slave方式來解決狐血,zookeeper本身自己是通過zab協(xié)議集群部署來解決的。

注意易核,DB和redis的復(fù)制一般都是異步的匈织,也就是說某些時(shí)刻分布式鎖發(fā)生故障可能存在數(shù)據(jù)不一致問題,而zookeeper本身通過zab協(xié)議保證集群內(nèi)(至少n/2+1個(gè))節(jié)點(diǎn)數(shù)據(jù)一致性。

?鎖喚醒:DB和redis分布式鎖一般不支持喚醒機(jī)制(也可以通過應(yīng)用層自己做輪詢檢測鎖是否空閑缀匕,空閑就喚醒內(nèi)部加鎖線程)纳决,zookeeper可通過本身的watcher/notify機(jī)制來做。


使用分布式鎖乡小,安全性上和多線程(同一個(gè)進(jìn)程內(nèi))加鎖是沒法比的阔加,可能由于網(wǎng)絡(luò)原因,分布式鎖服務(wù)(因?yàn)槌瑫r(shí)或者認(rèn)為client掛了)將加鎖資源給刪除了满钟,如果client端繼續(xù)操作共享資源胜榔,此時(shí)是有隱患的。

因此湃番,對于分布式鎖夭织,一個(gè)是盡量提高分布式鎖服務(wù)的可用性,另一個(gè)就是要部署同一內(nèi)網(wǎng)吠撮,盡量降低網(wǎng)絡(luò)問題發(fā)生幾率尊惰。這樣來看泥兰,貌似分布式鎖服務(wù)不是“完美”的(PS:技術(shù)貌似也不好做到十全十美 :( )弄屡,那么開發(fā)人員該如何選擇分布式鎖呢?最好是結(jié)合自己的業(yè)務(wù)實(shí)際場景鞋诗,來選擇不同的分布式鎖實(shí)現(xiàn)琢岩,一般來說,基于redis的分布式鎖服務(wù)應(yīng)用較多师脂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末担孔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子吃警,更是在濱河造成了極大的恐慌糕篇,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酌心,死亡現(xiàn)場離奇詭異拌消,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)安券,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門墩崩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侯勉,你說我怎么就攤上這事鹦筹。” “怎么了址貌?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵铐拐,是天一觀的道長徘键。 經(jīng)常有香客問我,道長遍蟋,這世上最難降的妖魔是什么吹害? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮虚青,結(jié)果婚禮上它呀,老公的妹妹穿的比我還像新娘。我一直安慰自己棒厘,他們只是感情好钟些,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绊谭,像睡著了一般政恍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上达传,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天篙耗,我揣著相機(jī)與錄音,去河邊找鬼宪赶。 笑死宗弯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的搂妻。 我是一名探鬼主播蒙保,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼欲主!你這毒婦竟也來了邓厕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤扁瓢,失蹤者是張志新(化名)和其女友劉穎详恼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體引几,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昧互,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伟桅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敞掘。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖楣铁,靈堂內(nèi)的尸體忽然破棺而出玖雁,到底是詐尸還是另有隱情,我是刑警寧澤民褂,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布茄菊,位于F島的核電站疯潭,受9級特大地震影響赊堪,放射性物質(zhì)發(fā)生泄漏面殖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一哭廉、第九天 我趴在偏房一處隱蔽的房頂上張望脊僚。 院中可真熱鬧,春花似錦遵绰、人聲如沸辽幌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乌企。三九已至,卻和暖如春成玫,著一層夾襖步出監(jiān)牢的瞬間加酵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工哭当, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留猪腕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓钦勘,卻偏偏與公主長得像陋葡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子彻采,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

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