ZooKeeper學習筆記四:使用ZooKeeper實現一個簡單的分散式鎖

Grey Zeng 發表於 2021-06-08
ZooKeeper

作者: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....

完整程式碼

Github