前言
分布式鎖峡谊,在實(shí)際的業(yè)務(wù)使用場景中算是比較常用的了茫虽,而分布式鎖的實(shí)現(xiàn),常見的除了redis之外靖苇,就是zk的實(shí)現(xiàn)了席噩;
下面簡單的創(chuàng)建一個分布式鎖。
依賴
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
實(shí)例創(chuàng)建
public class ZkLock implements Watcher {
private ZooKeeper zooKeeper;
// 創(chuàng)建一個持久的節(jié)點(diǎn)贤壁,作為分布式鎖的根目錄
private String root;
public ZkLock(String root) throws IOException {
try {
this.root = root;
zooKeeper = new ZooKeeper("127.0.0.1:2181", 500_000, this);
Stat stat = zooKeeper.exists(root, false);
if (stat == null) {
// 不存在則創(chuàng)建
createNode(root, true);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 簡單的封裝節(jié)點(diǎn)創(chuàng)建悼枢,這里只考慮持久 + 臨時順序
private String createNode(String path, boolean persistent) throws Exception {
return zooKeeper.create(path, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, persistent ? CreateMode.PERSISTENT : CreateMode.EPHEMERAL_SEQUENTIAL);
}
}
需要持有當(dāng)前節(jié)點(diǎn)和監(jiān)聽前一個節(jié)點(diǎn)的變更,所以我們在ZkLock實(shí)例中脾拆,添加兩個成員馒索;
/**
* 當(dāng)前節(jié)點(diǎn)
*/
private String current;
/**
* 前一個節(jié)點(diǎn)
*/
private String pre;
嘗試獲取鎖的邏輯
- current不存在,在表示沒有創(chuàng)建過名船,就創(chuàng)建一個臨時順序節(jié)點(diǎn)绰上,并賦值current
- current存在,則表示之前已經(jīng)創(chuàng)建過了渠驼,目前處于等待鎖釋放過程
- 接下來根據(jù)當(dāng)前節(jié)點(diǎn)順序是否最小蜈块,來表明是否持有鎖成功
- 當(dāng)順序不是最小時,找前面那個節(jié)點(diǎn)迷扇,并賦值 pre
- 監(jiān)聽pre的變化
/**
* 嘗試獲取鎖百揭,創(chuàng)建順序臨時節(jié)點(diǎn),若數(shù)據(jù)最小蜓席,則表示搶占鎖成功器一;否則失敗
*
* @return
*/
public boolean tryLock() {
try {
String path = root + "/";
if (current == null) {
// 創(chuàng)建臨時順序節(jié)點(diǎn)
current = createNode(path, false);
}
List<String> list = zooKeeper.getChildren(root, false);
Collections.sort(list);
if (current.equalsIgnoreCase(path + list.get(0))) {
// 獲取鎖成功
return true;
} else {
// 獲取鎖失敗,找到前一個節(jié)點(diǎn)
int index = Collections.binarySearch(list, current.substring(path.length()));
// 查詢當(dāng)前節(jié)點(diǎn)前面的那個
pre = path + list.get(index - 1);
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
注意上面的實(shí)現(xiàn)厨内,這里并沒有去監(jiān)聽前一個節(jié)點(diǎn)的變更祈秕,在設(shè)計tryLock,因?yàn)槭橇ⅠR返回成功or失敗雏胃,所以使用這個接口的请毛,不需要注冊監(jiān)聽。
我們的監(jiān)聽邏輯瞭亮,放在 lock() 同步阻塞里面获印;
- 嘗試搶占鎖,成功則直接返回
- 拿鎖失敗,則監(jiān)聽前一個節(jié)點(diǎn)的刪除事件
public boolean lock() {
if (tryLock()) {
return true;
}
try {
// 監(jiān)聽前一個節(jié)點(diǎn)的刪除事件
Stat state = zooKeeper.exists(pre, true);
if (state != null) {
synchronized (pre) {
// 阻塞等待前面的節(jié)點(diǎn)釋放
pre.wait();
// 這里不直接返回true兼丰,因?yàn)榍懊娴囊粋€節(jié)點(diǎn)刪除玻孟,可能并不是因?yàn)樗钟墟i并釋放鎖,如果是因?yàn)檫@個會話中斷導(dǎo)致臨時節(jié)點(diǎn)刪除鳍征,這個時候需要做的是換一下監(jiān)聽的 preNode
return lock();
}
} else {
// 不存在黍翎,則再次嘗試拿鎖
return lock();
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
注意:
- 當(dāng)節(jié)點(diǎn)不存在時,或者事件觸發(fā)回調(diào)之后艳丛,重新調(diào)用lock()匣掸,表明我胡漢三又來競爭鎖了?
為啥不是直接返回 true? 而是需要重新競爭呢氮双?- 因?yàn)榍懊婀?jié)點(diǎn)的刪除碰酝,有可能是因?yàn)榍懊婀?jié)點(diǎn)的會話中斷導(dǎo)致的;但是鎖還在另外的實(shí)例手中戴差,這個時候我應(yīng)該做的是重新排隊
最后別忘了釋放鎖