多線程情況下對共享資源的操作需要加鎖牵现,避免數(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)用較多师脂。