在Zookeeper實現分布式鎖的第一篇文章Zookeeper實現分布式鎖(一)While版中,我們使用Zookeeper實現了一個分布式鎖,過程原理如下:
1.所有客戶端都使用create方法創建同一個名字的節點
2.創建成功的獲取到鎖,其余的客戶端不斷while循環重試
3.獲取到鎖的客戶端執行完邏輯代碼后delete節點,其余客戶端繼續爭搶
這種方式會存在一下問題:
1.while循環浪費cpu
2.頻繁進行create重試會對Zookeeper服務器造成壓力
針對以上問題我們可以通過Zookeeper的watcher來解決,首先我們來看watcher的使用。
Watcher使用
在zookeeper中,watcher機制是當服務端的節點信息發生了變化時通知所有監聽了該節點的客戶端。其允許客戶端向服務端注冊一個watcher監聽,當服務端的一些指定事件觸發了這個watcher,就會向指定的客戶端發送一個事件通知。
public static void watch() throws Exception{
zk.create("/zx3", "aaa".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.exists("/zx3", new Watcher() {
public void process(WatchedEvent watchedEvent) {
try {
System.out.println("監聽的節點有變化,觸發事件");
System.out.println(watchedEvent.getPath());
System.out.println(watchedEvent.getState());
System.out.println(watchedEvent.getType());
} catch (Exception e) {
// TODO: handle exception
}
}
});
//通過刪除/zx3節點來觸發watcher事件
zk.delete("/zx3", -1);
}
輸出結果
監聽的節點有變化,觸發事件
/zx3
SyncConnected
NodeDeleted
通過watcher的這種特性我們可以避免while循環不斷重試的弊端。
Watcher鎖實現
根據watcher可以監聽某個節點的變化這個特點,可以用來優化我們的while鎖。
1.實現原理
1.所有客戶端都使用create方法創建同一個名字的節點
2.創建成功的獲取到鎖,失敗的客戶端對該節點設置一個watcher
3.獲取到鎖的客戶端delete節點,其余客戶端收到watcher的通知,此時再執行一次create方法
4.1-3進行循環
上述方式避免了獲取不到鎖的客戶端不斷循環重試的風險,而是當有人釋放鎖的時候,通知剩下的所有人來嘗試獲取鎖。
2.獲取方法
public boolean lock() throws Exception {
String path = null;
while (true) {
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
path = zk.create(nodeString, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (KeeperException e) {
System.out.println(Thread.currentThread().getName() + " 請求失敗");
try {
zk.exists(nodeString, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("事件來了:" + watchedEvent.toString());
if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
System.out.println("delete事件");
countDownLatch.countDown();
}
}
});
} catch (KeeperException e1) {
e.printStackTrace();
}
}
if (path != null && !path.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 拿到 Lock...");
break;
}
countDownLatch.await();
}
return true;
}
上述代碼還是使用了while循環?
代碼邏輯:
1.首先嘗試create節點獲取鎖,如果拿鎖失敗,就通過watcher監聽nodeString節點
2.在while循環最后使用countDownLatch.await()阻止繼續循環
3.直到watcher收到釋放鎖的通知才countDownLatch.countDown()解開阻塞,重新執行create獲取鎖
4.重復1-3
3.釋放鎖
public void unlock() {
try {
zk.delete(nodeString, -1);
System.out.println(Thread.currentThread().getName() + "釋放 Lock...");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
4.main方法
public static void main(String args[]) throws InterruptedException {
ZookeeperLock_2_Watch test = new ZookeeperLock_2_Watch();
try {
Thread.sleep(100);
zk.create(lockNameSpace, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} catch (KeeperException e) {
} catch (InterruptedException e) {
}
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 4; i++) {
service.execute(() -> {
try {
test.lock();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
test.unlock();
});
}
service.shutdown();
}
5.輸出結果
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
pool-1-thread-2 拿到 Lock...
pool-1-thread-4 請求失敗
pool-1-thread-1 請求失敗
pool-1-thread-3 請求失敗
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
pool-1-thread-2釋放 Lock...
pool-1-thread-1 拿到 Lock...
pool-1-thread-4 請求失敗
pool-1-thread-3 請求失敗
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
pool-1-thread-1釋放 Lock...
pool-1-thread-4 拿到 Lock...
pool-1-thread-3 請求失敗
事件來了:WatchedEvent state:SyncConnected type:NodeDeleted path:/mylock/lock1
delete事件
pool-1-thread-4釋放 Lock...
pool-1-thread-3 拿到 Lock...
pool-1-thread-3釋放 Lock...
通過輸出結果可以看到,只要有一個客戶端拿到鎖,其余的客戶端都不會再重復請求,減少了cpu和zookeeper的壓力,當釋放鎖時,zookeeper會通知所有監聽的客戶端,客戶端再執行create來競爭鎖,完美解決了while版本的問題。
存在的問題
通過Watcher實現的鎖雖然解決了While的無限循環問題,但還是存在一個比較棘手的麻煩:
當有極多的客戶端在watcher等待鎖時,一旦持有鎖的客戶端釋放,就會引起“驚群效應”,無數個客戶端的請求壓力直接打到zookeeper服務器上