如何使用zookeeper實現(xiàn)分布式鎖,在描述算法流程之前,先看下zookeeper中幾個關(guān)于節(jié)點的有趣的性質(zhì):
有序節(jié)點:假如當(dāng)前有一個父節(jié)點為/lock,我們可以在這個父節(jié)點下面創(chuàng)建子節(jié)點趁蕊;zookeeper提供了一個可選的有序特性缀棍,例如我們可以創(chuàng)建子節(jié)點“/lock/node-”并且指明有序,那么zookeeper在生成子節(jié)點時會根據(jù)當(dāng)前的子節(jié)點數(shù)量自動添加整數(shù)序號捌肴,也就是說如果是第一個創(chuàng)建的子節(jié)點枪眉,那么生成的子節(jié)點為/lock/node-0000000000捺檬,下一個節(jié)點則為/lock/node-0000000001,依次類推贸铜。
臨時節(jié)點:客戶端可以建立一個臨時節(jié)點堡纬,在會話結(jié)束或者會話超時后聂受,zookeeper會自動刪除該節(jié)點。
事件監(jiān)聽:在讀取數(shù)據(jù)時烤镐,我們可以同時對節(jié)點設(shè)置事件監(jiān)聽蛋济,當(dāng)節(jié)點數(shù)據(jù)或結(jié)構(gòu)變化時,zookeeper會通知客戶端炮叶。當(dāng)前zookeeper有如下四種事件:1)節(jié)點創(chuàng)建碗旅;2)節(jié)點刪除;3)節(jié)點數(shù)據(jù)修改镜悉;4)子節(jié)點變更祟辟。
下面描述使用zookeeper實現(xiàn)分布式鎖的算法流程,假設(shè)鎖空間的根節(jié)點為/lock:
客戶端連接zookeeper侣肄,并在/lock下創(chuàng)建臨時的且有序的子節(jié)點川尖,第一個客戶端對應(yīng)的子節(jié)點為/lock/lock-0000000000,第二個為/lock/lock-0000000001茫孔,以此類推。
客戶端獲取/lock下的子節(jié)點列表被芳,判斷自己創(chuàng)建的子節(jié)點是否為當(dāng)前子節(jié)點列表中序號最小的子節(jié)點缰贝,如果是則認(rèn)為獲得鎖,否則監(jiān)聽/lock的子節(jié)點變更消息畔濒,獲得子節(jié)點變更通知后重復(fù)此步驟直至獲得鎖剩晴;
執(zhí)行業(yè)務(wù)代碼;
完成業(yè)務(wù)流程后侵状,刪除對應(yīng)的子節(jié)點釋放鎖赞弥。
步驟1中創(chuàng)建的臨時節(jié)點能夠保證在故障的情況下鎖也能被釋放,考慮這么個場景:假如客戶端a當(dāng)前創(chuàng)建的子節(jié)點為序號最小的節(jié)點趣兄,獲得鎖之后客戶端所在機器宕機了绽左,客戶端沒有主動刪除子節(jié)點;如果創(chuàng)建的是永久的節(jié)點艇潭,那么這個鎖永遠不會釋放拼窥,導(dǎo)致死鎖;由于創(chuàng)建的是臨時節(jié)點蹋凝,客戶端宕機后鲁纠,過了一定時間zookeeper沒有收到客戶端的心跳包判斷會話失效,將臨時節(jié)點刪除從而釋放鎖鳍寂。
另外細(xì)心的朋友可能會想到改含,在步驟2中獲取子節(jié)點列表與設(shè)置監(jiān)聽這兩步操作的原子性問題,考慮這么個場景:客戶端a對應(yīng)子節(jié)點為/lock/lock-0000000000迄汛,客戶端b對應(yīng)子節(jié)點為/lock/lock-0000000001捍壤,客戶端b獲取子節(jié)點列表時發(fā)現(xiàn)自己不是序號最小的骤视,但是在設(shè)置監(jiān)聽器前客戶端a完成業(yè)務(wù)流程刪除了子節(jié)點/lock/lock-0000000000,客戶端b設(shè)置的監(jiān)聽器豈不是丟失了這個事件從而導(dǎo)致永遠等待了白群?這個問題不存在的尚胞。因為zookeeper提供的API中設(shè)置監(jiān)聽器的操作與讀操作是原子執(zhí)行的,也就是說在讀子節(jié)點列表時同時設(shè)置監(jiān)聽器帜慢,保證不會丟失事件笼裳。
最后,對于這個算法有個極大的優(yōu)化點:假如當(dāng)前有1000個節(jié)點在等待鎖粱玲,如果獲得鎖的客戶端釋放鎖時躬柬,這1000個客戶端都會被喚醒,這種情況稱為“羊群效應(yīng)”抽减;在這種羊群效應(yīng)中允青,zookeeper需要通知1000個客戶端,這會阻塞其他的操作卵沉,最好的情況應(yīng)該只喚醒新的最小節(jié)點對應(yīng)的客戶端颠锉。應(yīng)該怎么做呢?在設(shè)置事件監(jiān)聽時史汗,每個客戶端應(yīng)該對剛好在它之前的子節(jié)點設(shè)置事件監(jiān)聽琼掠,例如子節(jié)點列表為/lock/lock-0000000000、/lock/lock-0000000001停撞、/lock/lock-0000000002瓷蛙,序號為1的客戶端監(jiān)聽序號為0的子節(jié)點刪除消息,序號為2的監(jiān)聽序號為1的子節(jié)點刪除消息。
所以調(diào)整后的分布式鎖算法流程如下:
客戶端連接zookeeper,并在/lock下創(chuàng)建臨時的且有序的子節(jié)點愕宋,第一個客戶端對應(yīng)的子節(jié)點為/lock/lock-0000000000,第二個為/lock/lock-0000000001冠桃,以此類推。
客戶端獲取/lock下的子節(jié)點列表恐疲,判斷自己創(chuàng)建的子節(jié)點是否為當(dāng)前子節(jié)點列表中序號最小的子節(jié)點腊满,如果是則認(rèn)為獲得鎖,否則監(jiān)聽剛好在自己之前一位的子節(jié)點刪除消息培己,獲得子節(jié)點變更通知后重復(fù)此步驟直至獲得鎖碳蛋;
執(zhí)行業(yè)務(wù)代碼;
完成業(yè)務(wù)流程后省咨,刪除對應(yīng)的子節(jié)點釋放鎖肃弟。
接下來我們來看看在代碼中如何實現(xiàn)
package com.zdnst.boot.zk;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
/**
* Created by apple on 2019/3/27.
*/
public class ZKClientDemo {
public static void main(String[] args){
String connection = "127.0.0.1:2181";
String LOCK_PATH = "/lock";
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client = CuratorFrameworkFactory.newClient(connection, 3000, 3000,retryPolicy);
client.start();
InterProcessMutex mutex = new InterProcessMutex(client,LOCK_PATH);
try {
mutex.acquire();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
mutex.release();
} catch (Exception e) {
e.printStackTrace();
}finally {
client.close();
}
}
}
}
從代碼來看實現(xiàn)一個分布式鎖原來如此簡單。