在Zookeeper實現(xiàn)分布式鎖的前兩篇文章
Zookeeper實現(xiàn)分布式鎖(一)While版
Zookeeper實現(xiàn)分布式鎖(二)Watcher版
我們實現(xiàn)了兩種類型的鎖,第一種是while循環(huán),由于無限循環(huán)帶來的cpu和zookeeper服務(wù)器的消耗,我們使用了watcher的方式,watcher方式采用了客戶端監(jiān)聽鎖節(jié)點的方式,完美解決了while出現(xiàn)的問題,但是這種方式又會存在“驚群”效應(yīng),本篇文章我們來實現(xiàn)一種排隊鎖也就是公平鎖。
Zookeeper的節(jié)點類型
zookeeper實現(xiàn)公平鎖依賴于zookeeper的順序節(jié)點,先來了解一下zookeeper的節(jié)點都有什么類型的節(jié)點。
一共有四種
EPHEMERAL:臨時節(jié)點,當(dāng)客戶端與ZooKeeper集合斷開連接時,臨時節(jié)點會自動刪除。
PERSISTENT:持久節(jié)點,即使在創(chuàng)建該節(jié)點的客戶端斷開連接后,持久節(jié)點仍然存在。
EPHEMERAL_SEQUENTIAL:臨時順序節(jié)點
PERSISTENT_SEQUENTIAL:持久順序節(jié)點
重點說一下順序節(jié)點的含義:順序節(jié)點可以是持久的或臨時的。當(dāng)一個新的znode被創(chuàng)建為一個順序節(jié)點時,ZooKeeper通過將10位的序列號附加到原始名稱來設(shè)置znode的路徑。例如,如果將具有路徑 /myapp 的znode創(chuàng)建為順序節(jié)點,則ZooKeeper會將路徑更改為 /myapp0000000001 ,并將下一個序列號設(shè)置為0000000002。如果兩個順序節(jié)點是同時創(chuàng)建的,那么ZooKeeper不會對每個znode使用相同的數(shù)字。
根據(jù)同時創(chuàng)建的同名順序節(jié)點zookeeper會自動在命名上排序的特性,可以實現(xiàn)我們的公平鎖。
FairLock實現(xiàn)
1.獲取鎖
多個客戶端在lockName目錄下創(chuàng)建同名的臨時順序節(jié)點,因為zookeeper會為我們保證節(jié)點的命名不重復(fù)性和順序性,所以可以利用節(jié)點的順序進(jìn)行鎖的判斷。
public void lock(){
String path = null;
try {
path = zk.create(lockName+"/mylock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
lockZnode = path;
List<String> minPath = zk.getChildren(lockName,false);
System.out.println(minPath);
Collections.sort(minPath);
System.out.println("最小的節(jié)點是:"+minPath.get(0));
if (path!=null&&!path.isEmpty()
&&minPath.get(0)!=null&&!minPath.get(0).isEmpty()
&&path.equals(lockName+"/"+minPath.get(0))) {
System.out.println(Thread.currentThread().getName() + " 獲取鎖...");
return;
}
String watchNode = null;
for (int i=minPath.size()-1;i>=0;i--){
if(minPath.get(i).compareTo(path.substring(path.lastIndexOf("/") + 1))<0){
watchNode = minPath.get(i);
break;
}
}
if (watchNode!=null){
final String watchNodeTmp = watchNode;
final Thread thread = Thread.currentThread();
Stat stat = zk.exists(lockName + "/" + watchNodeTmp,new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType() == Event.EventType.NodeDeleted){
System.out.println("delete事件來了");
thread.interrupt();
System.out.println("打斷當(dāng)前線程");
}
}
});
if(stat != null){
System.out.println(Thread.currentThread().getName() + " waiting for " + lockName + "/" + watchNode);
}
}
try {
Thread.sleep(10000);
}catch (InterruptedException ex){
System.out.println(Thread.currentThread().getName() + " 被喚醒");
System.out.println(Thread.currentThread().getName() + " 獲取鎖...");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
獲取鎖邏輯:
1.首先創(chuàng)建順序節(jié)點,然后獲取當(dāng)前目錄下最小的節(jié)點,判斷最小節(jié)點是不是當(dāng)前節(jié)點,如果是那么獲取鎖成功,如果不是那么獲取鎖失敗。
2.獲取鎖失敗的節(jié)點獲取當(dāng)前節(jié)點上一個順序節(jié)點,對此節(jié)點注冊watcher監(jiān)聽,并使當(dāng)前線程進(jìn)入sleep狀態(tài)。
3.當(dāng)監(jiān)聽的節(jié)點unlock刪除節(jié)點之后會捕獲到delete事件,這說明前面的線程都執(zhí)行完了,當(dāng)前線程interrupt,打斷sleep狀態(tài),獲取鎖。
2.釋放鎖
public void unlock(){
try {
System.out.println(Thread.currentThread().getName() + "釋放 Lock...");
zk.delete(lockZnode,-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
3.輸出結(jié)果
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
[mylock_0000000112]
最小的節(jié)點是:mylock_0000000112
pool-1-thread-2 獲取鎖...
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的節(jié)點是:mylock_0000000112
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的節(jié)點是:mylock_0000000112
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的節(jié)點是:mylock_0000000112
pool-1-thread-2釋放 Lock...
pool-1-thread-1 waiting for /mylock/mylock_0000000113
pool-1-thread-3 waiting for /mylock/mylock_0000000114
pool-1-thread-4 waiting for /mylock/mylock_0000000112
delete事件來了
打斷當(dāng)前線程
pool-1-thread-4 被喚醒
pool-1-thread-4 獲取鎖...
pool-1-thread-4釋放 Lock...
delete事件來了
打斷當(dāng)前線程
pool-1-thread-1 被喚醒
pool-1-thread-1 獲取鎖...
pool-1-thread-1釋放 Lock...
delete事件來了
打斷當(dāng)前線程
pool-1-thread-3 被喚醒
pool-1-thread-3 獲取鎖...
pool-1-thread-3釋放 Lock...
總結(jié)
根據(jù)輸出結(jié)果,可以看出來這次的客戶端獲取鎖是公平的,排著隊一個一個來的,因為每個節(jié)點都有自己的一個數(shù)字id,根據(jù)數(shù)字id來監(jiān)聽比自己小的節(jié)點,這樣釋放鎖只喚醒一個客戶端,而不會產(chǎn)生驚群效應(yīng)。
while版、watcher版、FairLock版這三種鎖
while:性能好,但是資源消耗大,適合業(yè)務(wù)邏輯時間短的場景
watcher:性能中上,但是容易驚群,適合客戶端比較少的場景
FairLock:性能低,但是安全性高,適合速度要求一般,按順序來的場景