六、分佈式鎖的實現原理
實現分佈式鎖的方式有許多種,比如使用數據庫鎖、redis等等。而Zookeeper也可以用於實現分佈式鎖,下面通過源碼來介紹Zookeeper是如何來實現分佈式鎖的?
6.1 驚羣效應
假設用節點"/lock"來表示這個分佈式鎖,我們第一時間可能會想到通過判斷節點"/lock"存在與否,如果不存在則創建該節點,表示獲取到這個鎖,否則監聽該節點,等待鎖的釋放。如下圖所示
這種方案在客户端數據較多的時候,所有客户端都監聽該"/lock"節點,而當"/lock"節點移除時,zk服務需要做大量的通知處理,導致zk服務的性能驟降。這種現象就叫驚羣效應(網上很多地方翻譯成羊羣效應,個人感覺驚羣效應更貼切一些)
6.2 實現原理
在Zookeeper的源碼中已經有分佈式鎖的實現,在zookeeper-recipes/zookeeper-recipes-lock目錄下。在節點"/lock"下的子節點都是EPHEMERAL_SEQUENTIAL臨時順序節點,第一個子節點為持有當前分佈式鎖的線程,後續的子節點監聽前一個子節點,這樣就解決了驚羣效應的問題。
其實現的示意圖如下:
大致步驟可以分為以下幾步:
- 判斷/lock下面是否有子節點
- 如果沒有,説明當前分佈式鎖未被持有,則創建臨時順序子節點,節點名稱為
x-sessionId-序列號。 - 如果有,説明當前分佈式鎖已被持有,在該目錄下創建臨時順序節點,並監聽它的上一個子節點
- 當第一個子節點釋放鎖,第二個節點監聽到後,重新調用lock()方法判斷當前是不是拿到了鎖。如此反覆
6.3 源碼解讀
調用lock()方法來獲取分佈式鎖,源碼如下
public synchronized boolean lock() throws KeeperException, InterruptedException {
if (isClosed()) {
return false;
}
// 確保路徑存在,不存在則創建
ensurePathExists(dir);
// 嘗試獲取鎖
return (Boolean) retryOperation(zop);
}
底層調用LockZooKeeperOperation類的execute()方法,嘗試獲取鎖,如果拿不到,則監聽上一個子節點,代碼如下
public boolean execute() throws KeeperException, InterruptedException {
do {
if (id == null) {
long sessionId = zookeeper.getSessionId();
String prefix = "x-" + sessionId + "-";
// 判斷該會話是否之前已經有創建子節點,如果有,則拿之前創建好的id名,否則新建子節點
findPrefixInChildren(prefix, zookeeper, dir);
idName = new ZNodeName(id);
}
List<String> names = zookeeper.getChildren(dir, false);
if (names.isEmpty()) {
LOG.warn("No children in: {} when we've just created one! Lets recreate it...", dir);
// 重建節點
id = null;
} else {
// 對這些子節點id進行排序,並拿到比當前子節點id小的列表
SortedSet<ZNodeName> sortedNames = new TreeSet<>();
for (String name : names) {
sortedNames.add(new ZNodeName(dir + "/" + name));
}
ownerId = sortedNames.first().getName();
SortedSet<ZNodeName> lessThanMe = sortedNames.headSet(idName);
// 如果比當前子節點id小的列表不為空,則監聽上一個子節點
if (!lessThanMe.isEmpty()) {
ZNodeName lastChildName = lessThanMe.last();
lastChildId = lastChildName.getName();
LOG.debug("Watching less than me node: {}", lastChildId);
Stat stat = zookeeper.exists(lastChildId, new LockWatcher());
if (stat != null) {
return Boolean.FALSE;
} else {
LOG.warn("Could not find the stats for less than me: {}", lastChildName.getName());
}
} else {
// 否則判斷當前子節點id是不是最小id,如果是,則返回true,表示獲取到鎖
if (isOwner()) {
LockListener lockListener = getLockListener();
if (lockListener != null) {
lockListener.lockAcquired();
}
return Boolean.TRUE;
}
}
}
}
while (id == null);
return Boolean.FALSE;
}
當客户端釋放鎖時,調用unlock()方法,刪除當前對應id的子節點
public synchronized void unlock() throws RuntimeException {
if (!isClosed() && id != null) {
try {
ZooKeeperOperation zopdel = () -> {
// 刪除當前對應id的子節點
zookeeper.delete(id, -1);
return Boolean.TRUE;
};
zopdel.execute();
} catch (InterruptedException e) {
// 略
} finally {
LockListener lockListener = getLockListener();
if (lockListener != null) {
lockListener.lockReleased();
}
id = null;
}
}
}