作者:Grey
原文地址: ZooKeeper學習筆記四:使用ZooKeeper實現一個簡單的分散式鎖
前置知識
完成ZooKeeper叢集搭建以及熟悉ZooKeeperAPI基本使用
需求
當多個程式不在同一個系統中,用分散式鎖控制多個程式對資源的訪問。
在單機情況下,可以使用JUC包裡面的工具來進行互斥控制。
但是在分散式系統後,由於分散式系統多執行緒、多程式並且分佈在不同機器上,這將使原單機併發控制鎖策略失效,為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分散式鎖的由來。
當多個程式不在同一個系統中,就需要用分散式鎖控制多個程式對資源的訪問。
我們可以用ZooKeeper來模擬實現一個簡單的分散式鎖
環境準備
一個zk集權,ip和埠分別為:
- 192.168.205.145:2181
- 192.168.205.146:2181
- 192.168.205.147:2181
- 192.168.205.148:2181
定義主方法
App.java
public class App {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
ZkLock lock = new ZkLock();
lock.lock(); // 開啟鎖
System.out.println(Thread.currentThread().getName() + " doing work");
lock.release(); // 釋放鎖
}).start();
}
while (true) {
}
}
}
如上,我們設計了一個ZkLock,其中lock方法是鎖定資源,release方法是釋放資源,我們併發了10個執行緒併發訪問來模擬。
public class ZkLock implements AsyncCallback.StringCallback, Watcher, AsyncCallback.StatCallback, AsyncCallback.Children2Callback {
private CountDownLatch latch;
private ZooKeeper zk;
private String identify;
private String lockPath;
private String pathName;
public ZkLock() {
identify = Thread.currentThread().getName();
lockPath = "/lock";
latch = new CountDownLatch(1);
zk = ZookeeperConfig.create(ADDRESS + "/testLock");
}
public void lock() {
try {
zk.create(lockPath, currentThread().getName().getBytes(UTF_8), OPEN_ACL_UNSAFE, EPHEMERAL_SEQUENTIAL, this, currentThread().getName());
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void release() {
try {
zk.delete(pathName, -1);
System.out.println(identify + " over work....");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
@Override
public void processResult(int rc, String path, Object ctx, String name) {
if (null != name) {
// 建立成功
System.out.println(identify + " created " + name);
pathName = name;
zk.getChildren("/", false, this, "dasdfas");
}
}
@Override
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
sort(children);
int i = children.indexOf(pathName.substring(1));
if (i == 0) {
// 是第一個,獲得鎖,可以執行
System.out.println(identify + " first...");
try {
zk.setData("/", identify.getBytes(UTF_8), -1);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
} else {
zk.exists("/" + children.get(i - 1), this, this, "ddsdf");
}
}
@Override
public void process(WatchedEvent event) {
switch (event.getType()) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
zk.getChildren("/", false, this, "sdf");
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
}
}
關於上述程式碼的說明,我們規定建立的zk目錄為/testLock,所以我們可以通過zk客戶端在叢集中先把/testLock目錄建好,後續執行緒爭搶的時候,我們只需要建立序列化的臨時節點(以/lock開頭),因為是序列化的,所以我們可以設定讓第一個建立好節點的執行緒搶到鎖,其他的執行緒排隊等待。
所以lock方法實現如下:
zk.create(lockPath, currentThread().getName().getBytes(UTF_8), OPEN_ACL_UNSAFE, EPHEMERAL_SEQUENTIAL, this, currentThread().getName());
lock方法在執行的時候,會有一個回撥,即:當節點建立成功後,會判斷/testLock節點中有沒有已經建立好的且在當前節點之前的節點,有的話,則註冊一個一個對於/testLock目錄的監聽:
@Override
public void processResult(int rc, String path, Object ctx, String name) {
if (null != name) {
// 建立成功
System.out.println(identify + " created " + name);
pathName = name;
zk.getChildren("/", false, this, "dasdfas");
}
}
一旦發現/testLock目錄下已經有節點了,那麼我們拿到/testLock下的所有節點,並排序,取最小的那個節點執行即可:
@Override
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
sort(children);
int i = children.indexOf(pathName.substring(1));
if (i == 0) {
// 是第一個,獲得鎖,可以執行
System.out.println(identify + " first...");
try {
zk.setData("/", identify.getBytes(UTF_8), -1);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
} else {
zk.exists("/" + children.get(i - 1), this, this, "ddsdf");
}
}
release方法很簡單,只需要把當前執行完畢的節點刪除即可:
public void release() {
try {
zk.delete(pathName, -1);
System.out.println(identify + " over work....");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
執行效果
確保zk中有/testLock這個節點,如果沒有,請先建立一個:
Run App.java
可以看到控制檯輸出:
Thread-5 created /lock0000000000
Thread-4 created /lock0000000001
Thread-1 created /lock0000000002
Thread-9 created /lock0000000003
Thread-6 created /lock0000000004
Thread-2 created /lock0000000005
Thread-3 created /lock0000000006
Thread-0 created /lock0000000007
Thread-8 created /lock0000000008
Thread-7 created /lock0000000009
Thread-5 first...
Thread-5 doing work
Thread-5 over work....
Thread-4 first...
Thread-4 doing work
Thread-4 over work....
Thread-1 first...
Thread-1 doing work
Thread-1 over work....
Thread-9 first...
Thread-9 doing work
Thread-9 over work....
Thread-6 first...
Thread-6 doing work
Thread-6 over work....
Thread-2 first...
Thread-2 doing work
Thread-2 over work....
Thread-3 first...
Thread-3 doing work
Thread-3 over work....
Thread-0 first...
Thread-0 doing work
Thread-0 over work....
Thread-8 first...
Thread-8 doing work
Thread-8 over work....
Thread-7 first...
Thread-7 doing work
Thread-7 over work....