分佈式系統筆記:利用zookeeper實現分佈式任務鎖(Java)

利用zookeeper實現分佈式任務鎖

依賴原理

  • 在ZK中添加基本節點,路徑爲鎖名稱,節點類型爲持久節點(PERSISTENT)。
  • 對需要獲取鎖的每個線程,在ZK中分別添加基本節點的子節點,路徑程序自定爲temp,類型爲臨時自編號節點(EPHEMERAL_SEQUENTIAL),並保存創建返回的實際節點路徑。
  • 通過delete方式刪除本線程創建的子節點,可以作爲鎖釋放的方式。
  • 基本節點的子節點類型爲臨時自編號節點(EPHEMERAL_SEQUENTIAL),當線程與ZK連接中斷後,ZK會自動將該節點刪除,確保了斷連之後的鎖釋放。
  • 由於ZK自編號產生的路徑是遞增的,因此可以通過判斷基本節點的子節點中最小路徑數字編號的節點是否是本線程新建的節點來判斷是否獲取到鎖。

原理圖示

利用zk實現的分佈式任務鎖實現原理如下:

8個線程分別嘗試獲取分佈式任務鎖,情況如下:
- (1)8個線程分別在ZK基本節點下創建臨時自編號節點,獲取創建成功後的實際路徑
- (2)在基本節點子節點列表中,判斷本線程創建節點編號是否爲最小
- (3)最小編號線程獲取分佈式任務鎖,執行臨界區程序,完成任務

這裏寫圖片描述

線程執行完任務,釋放鎖,情況如下:
- (1)線程釋放鎖,將ZK中對應的臨時節點刪除,此時基本節點下路徑最小的子節點獲取分佈式任務鎖
- (2)某線程由於網絡原因與ZK斷開了連接,退出鎖競爭,ZK自動將其對應的臨時節點刪除
- (3)新出現的線程加入鎖競爭,在ZK下創建臨時節點,排隊等待鎖競爭

這裏寫圖片描述

方案一 :輪詢方式

實現原理

程序流程圖如下:

這裏寫圖片描述

實現代碼

import org.apache.zookeeper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * 基於zk的分佈式任務鎖
 * <pre>
 *  方案一:輪詢方式
 *
 *  實現思路:阻塞遞歸獲取父節點的子節點列表判斷是否獲取鎖
 *  1.連接ZK時候監聽連接結果,若連接不成功則將是否需要中斷標誌位置爲是,之後獲取鎖方法會直接返回false。
 *  2.創建以鎖名稱命名的持久化節點作爲父節點,在父節點下創建名稱固定(程序定義)的臨時節點,實際創建的節點路徑zookeeper服務器會進行自編號。
 *  3.獲取鎖方法中,方法會阻塞、遞歸判斷本線程對應子節點的路徑後綴編號是否是父節點下所有子節點中最小的,若是最小的獲取鎖,反之繼續阻塞。
 *  4.獲取鎖方法中,若查詢父節點的子節點列表出現異常,則退出阻塞狀態並直接返回false,退出鎖競爭。
 *  5.線程獲取鎖並執行完成任務後,釋放鎖(刪除對應的子節點)並釋放zookeeper連接。
 * </pre>
 * Created by xuyh at 2017/11/24 9:18.
 */
public class ZKLock {
    private Logger logger = LoggerFactory.getLogger(ZKLock.class);
    private static final String CHILD_NODE_PATH = "temp";
    private String baseLockPath;
    private String finalLockId;

    //是否需要中斷阻塞標誌位
    private boolean needInterrupt = false;
    //ZK是否連接成功標誌位
    private boolean connected = false;

    private String host = "127.0.0.1";
    private String port = "2181";
    private ZooKeeper zooKeeper;

    private ZKLock(String lock, String host, String port) {
        this.host = host;
        this.port = port;
        this.baseLockPath = "/" + lock;
    }

    /**
     * 新建鎖(連接ZK阻塞)
     *
     * @param host zk 服務ip
     * @param port zk 服務端口
     * @param lock 鎖名稱
     * @return
     */
    public static ZKLock create(String host, String port, String lock) {
        ZKLock zkLock = new ZKLock(lock, host, port);
        zkLock.connectZooKeeper();
        return zkLock;
    }

    /**
     * 獲取鎖(阻塞)
     *
     * @return true代表獲取到分佈式任務鎖
     */
    public boolean getLock() {
        return isSubPathMin();
    }

    /**
     * 釋放鎖
     *
     * @return true代表釋放鎖成功, 並切斷ZK連接
     */
    public boolean releaseLock() {
        try {
            if (zooKeeper != null && connected) {
                zooKeeper.delete(finalLockId, -1);
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
        return disconnectZooKeeper();
    }

    private boolean connectZooKeeper() {
        try {
            //連接ZK,並註冊連接狀態監聽
            zooKeeper = new ZooKeeper(host + ":" + port, 60000, event -> {
                if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                    needInterrupt = true;
                } else {
                    if (event.getType() == Watcher.Event.EventType.None) {//連接成功
                        connected = true;
                    }
                }
            });

            //等待異步連接成功,超過時間30s則退出等待,防止線程鎖死
            int i = 1;
            while (!connected) {
                if (i == 100)
                    break;
                Thread.sleep(300);
                i++;
            }

            if (connected) {
                //創建父節點
                if (zooKeeper.exists(baseLockPath, false) == null) {
                    zooKeeper.create(baseLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }

                //創建子節點
                finalLockId = zooKeeper.create(baseLockPath + "/" + CHILD_NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.EPHEMERAL_SEQUENTIAL);
            } else {
                needInterrupt = true;
                logger.warn("Connect zookeeper failed. Time consumes 30 s");
                return false;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    private boolean disconnectZooKeeper() {
        if (zooKeeper == null && !connected)
            return false;
        try {
            connected = false;
            zooKeeper.close();
        } catch (Exception e) {
            logger.warn(String.format("ZK disconnect failed. [%s]", e.getMessage()), e);
        }
        return true;
    }

    private boolean isSubPathMin() {
        if (!connected)
            return false;
        try {
            Thread.sleep(1000);
            List<String> childrenList = zooKeeper.getChildren(baseLockPath, false);

            if (needInterrupt) {
                disconnectZooKeeper();
                return false;
            }

            if (judgePathNumMin(childrenList)) {
                return true;
            } else {
                return isSubPathMin();
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            disconnectZooKeeper();
            return false;
        }
    }

    private boolean judgePathNumMin(List<String> paths) {
        if (paths.isEmpty())
            return true;
        if (paths.size() >= 2) {
            //對無序狀態的子節點路徑列表按照編號升序排序
            paths.sort((str1, str2) -> {
                int num1;
                int num2;
                String string1 = str1.substring(CHILD_NODE_PATH.length(), str1.length());
                String string2 = str2.substring(CHILD_NODE_PATH.length(), str2.length());
                num1 = Integer.parseInt(string1);
                num2 = Integer.parseInt(string2);
                if (num1 > num2) {
                    return 1;
                } else if (num1 < num2) {
                    return -1;
                } else {
                    return 0;
                }
            });
        }

        //判斷本線程的節點路徑是否是最小編號
        String minId = paths.get(0);
        return finalLockId.equals(baseLockPath + "/" + minId);
    }
}

測試

測試程序

private void testZKLockOneWithMultiThread() throws Exception {
    int threadCount = 20;
    List<TestThread> testThreads = new ArrayList<>();
    for (int i = 0; i < threadCount; i++) {
        testThreads.add(new TestThread("" + i, "127.0.0.1", "2181", "testThreadLock"));
    }
    testThreads.forEach(TestThread::start);
    Thread.sleep(100000);
}

private class TestThread extends Thread {
    private String host;
    private String port;
    private String lockPath;

    private String num;

    /**
     * @param threadNum 線程編號
     */
    public TestThread(String threadNum, String host, String port, String lockPath) {
        this.host = host;
        this.port = port;
        this.lockPath = lockPath;
        this.num = threadNum;
    }

    @Override
    public void run() {
        ZKLock zkLock = ZKLock.create(host, port, lockPath);
        if (zkLock.getLock()) {
            System.out.println(String.format("線程:[%s]獲取到任務鎖,並執行了任務", num));
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
        } else {
            System.out.println(String.format("線程:[%s]沒有獲取到任務鎖,放棄執行任務", num));
        }
        zkLock.releaseLock();
    }
}

結果:

線程:[17]獲取到任務鎖,並執行了任務
線程:[11]獲取到任務鎖,並執行了任務
線程:[1]獲取到任務鎖,並執行了任務
線程:[19]獲取到任務鎖,並執行了任務
線程:[14]獲取到任務鎖,並執行了任務
線程:[8]獲取到任務鎖,並執行了任務
線程:[0]獲取到任務鎖,並執行了任務
線程:[5]獲取到任務鎖,並執行了任務
線程:[10]獲取到任務鎖,並執行了任務
線程:[16]獲取到任務鎖,並執行了任務
線程:[13]獲取到任務鎖,並執行了任務
線程:[12]獲取到任務鎖,並執行了任務
線程:[6]獲取到任務鎖,並執行了任務
線程:[18]獲取到任務鎖,並執行了任務
線程:[9]獲取到任務鎖,並執行了任務
線程:[7]獲取到任務鎖,並執行了任務
線程:[4]獲取到任務鎖,並執行了任務
線程:[15]獲取到任務鎖,並執行了任務
線程:[3]獲取到任務鎖,並執行了任務
線程:[2]獲取到任務鎖,並執行了任務

方案一優劣

優點

  • 通過遞歸實現循環輪詢
  • 程序實現邏輯簡單易懂
  • 不需要實現監聽節點變動的watcher

劣勢

  • 每個在阻塞狀態下競爭鎖的線程,都需要在固定時間間隔查詢所有存活節點情況,導致網絡開銷巨大,資源浪費巨大

方案二 :父節點監聽方式

實現原理

程序流程圖如下:

這裏寫圖片描述

實現代碼

import org.apache.zookeeper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * 基於zk的分佈式任務鎖
 * <pre>
 *     方案二:父節點監聽方式
 *
 *     實現思路:監聽父節點狀態
 *     1.在父節點(持久化)下創建臨時節點,實際創建的節點路徑會根據數量進行自增(ZK自編號方式創建節點)。
 *     2.創建節點成功後,獲取父節點下的子節點列表,判斷本線程的路徑後綴編號是否是所有子節點中最小的,若是則獲取鎖,反之監聽父節點變動狀態(通過getChildren()方法註冊watcher)
 *     3.當父節點狀態變動(主要是子節點列表變動)後watcher會接收到通知,這時判斷父節點下的子節點的排序狀態,若滿足本線程的路徑後綴編號最小則獲取鎖,反之繼續註冊watcher監聽父節點狀態
 * </pre>
 * <p>
 * Created by xuyh at 2017/11/29 13:43.
 */
public class ZKLockTwo {
    private Logger logger = LoggerFactory.getLogger(ZKLockTwo.class);
    private static final String CHILD_NODE_PATH = "temp";
    private String baseLockPath;
    private String finalLockId;

    //是否需要中斷阻塞標誌位
    private boolean needInterrupt = false;
    //ZK是否連接成功標誌位
    private boolean connected = false;
    //是否獲取到鎖標誌位
    private boolean acquireLock = false;

    private String host = "127.0.0.1";
    private String port = "2181";
    private ZooKeeper zooKeeper;
    private FatherNodeWatcher fatherNodeWatcher;

    private ZKLockTwo(String lock, String host, String port) {
        this.host = host;
        this.port = port;
        this.baseLockPath = "/" + lock;
        this.fatherNodeWatcher = new FatherNodeWatcher(this);
    }

    /**
     * 新建鎖(連接ZK阻塞)
     *
     * @param host zk 服務ip
     * @param port zk 服務端口
     * @param lock 鎖名稱
     * @return
     */
    public static ZKLockTwo create(String host, String port, String lock) {
        ZKLockTwo zkLockTwo = new ZKLockTwo(lock, host, port);
        zkLockTwo.connectZooKeeper();
        return zkLockTwo;
    }

    /**
     * 獲取鎖(阻塞)
     *
     * @return true代表獲取到分佈式任務鎖
     */
    public boolean getLock() {
        if (!connected)
            return false;
        while (!needInterrupt) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                logger.warn(e.getMessage(), e);
            }

            if (acquireLock) {
                return true;
            }
        }
        return false;
    }

    /**
     * 釋放鎖
     *
     * @return true代表釋放鎖成功, 並切斷ZK連接
     */
    public boolean releaseLock() {
        try {
            if (zooKeeper != null && connected) {
                zooKeeper.delete(finalLockId, -1);
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
        return disconnectZooKeeper();
    }

    private boolean disconnectZooKeeper() {
        if (zooKeeper == null && !connected)
            return false;
        try {
            connected = false;
            acquireLock = false;
            zooKeeper.close();
        } catch (Exception e) {
            logger.warn(String.format("ZK disconnect failed. [%s]", e.getMessage()), e);
        }
        return true;
    }

    private boolean connectZooKeeper() {
        try {
            //連接ZK
            zooKeeper = new ZooKeeper(host + ":" + port, 60000, event -> {
                if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                    needInterrupt = true;
                } else {
                    if (event.getType() == Watcher.Event.EventType.None) {//連接成功
                        connected = true;
                    }
                }
            });

            //等待異步連接成功,超過時間30s則退出等待,防止線程鎖死
            int i = 1;
            while (!connected) {
                if (i == 100)
                    break;
                Thread.sleep(300);
                i++;
            }

            if (connected) {
                //創建父節點
                if (zooKeeper.exists(baseLockPath, false) == null) {
                    zooKeeper.create(baseLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }

                //創建子節點
                finalLockId = zooKeeper.create(baseLockPath + "/" + CHILD_NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.EPHEMERAL_SEQUENTIAL);

                //檢查一次是否獲取到鎖
                checkAcquire();
            } else {
                needInterrupt = true;
                logger.warn("Connect zookeeper failed. Time consumes 30 s");
                return false;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    private void checkAcquire() {
        if (!connected)
            return;
        try {
            //獲取子節點列表同時再次註冊監聽
            List<String> childrenList = zooKeeper.getChildren(baseLockPath, fatherNodeWatcher);
            if (judgePathNumMin(childrenList)) {
                acquireLock = true;//獲取到鎖
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            disconnectZooKeeper();
        }
    }

    private boolean judgePathNumMin(List<String> paths) {
        if (paths.isEmpty())
            return true;
        if (paths.size() >= 2) {
            //對無序狀態的子節點路徑列表按照編號升序排序
            paths.sort((str1, str2) -> {
                int num1;
                int num2;
                String string1 = str1.substring(CHILD_NODE_PATH.length(), str1.length());
                String string2 = str2.substring(CHILD_NODE_PATH.length(), str2.length());
                num1 = Integer.parseInt(string1);
                num2 = Integer.parseInt(string2);
                if (num1 > num2) {
                    return 1;
                } else if (num1 < num2) {
                    return -1;
                } else {
                    return 0;
                }
            });
        }

        //判斷本線程的節點路徑是否是最小編號
        String minId = paths.get(0);
        return finalLockId.equals(baseLockPath + "/" + minId);
    }

    private class FatherNodeWatcher implements Watcher {
        private ZKLockTwo context;

        FatherNodeWatcher(ZKLockTwo context) {
            this.context = context;
        }

        @Override
        public void process(WatchedEvent event) {
            if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                context.needInterrupt = true;
            } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                context.needInterrupt = true;
            } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                context.needInterrupt = true;
            } else {
                if (event.getType() == Event.EventType.NodeChildrenChanged) {//子節點有變動
                    context.checkAcquire();
                }
            }
        }
    }
}


測試

測試程序

private void testZKLockTwoWithMultiThread() throws Exception {
    int threadCount = 20;
    List<TestLockTwoThread> testLockTwoThreads = new ArrayList<>();
    for (int i = 0; i < threadCount; i++) {
        testLockTwoThreads.add(new TestLockTwoThread("" + i, "127.0.0.1", "2181", "TestLockTwoThreadLock"));
    }
    testLockTwoThreads.forEach(TestLockTwoThread::start);
    Thread.sleep(100000);
}

private class TestLockTwoThread extends Thread {
    private String host;
    private String port;
    private String lockPath;

    private String num;

    /**
     * @param threadNum 線程編號
     */
    public TestLockTwoThread(String threadNum, String host, String port, String lockPath) {
        this.host = host;
        this.port = port;
        this.lockPath = lockPath;
        this.num = threadNum;
    }

    @Override
    public void run() {
        ZKLockTwo zkLockTwo = ZKLockTwo.create(host, port, lockPath);
        if (zkLockTwo.getLock()) {
            System.out.println(String.format("線程:[%s]獲取到任務鎖,並執行了任務", num));
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
        } else {
            System.out.println(String.format("線程:[%s]沒有獲取到任務鎖,放棄執行任務", num));
        }
        zkLockTwo.releaseLock();
    }
}

結果:

線程:[4]獲取到任務鎖,並執行了任務
線程:[8]獲取到任務鎖,並執行了任務
線程:[3]獲取到任務鎖,並執行了任務
線程:[2]獲取到任務鎖,並執行了任務
線程:[1]獲取到任務鎖,並執行了任務
線程:[0]獲取到任務鎖,並執行了任務
線程:[19]獲取到任務鎖,並執行了任務
線程:[17]獲取到任務鎖,並執行了任務
線程:[16]獲取到任務鎖,並執行了任務
線程:[15]獲取到任務鎖,並執行了任務
線程:[14]獲取到任務鎖,並執行了任務
線程:[12]獲取到任務鎖,並執行了任務
線程:[11]獲取到任務鎖,並執行了任務
線程:[10]獲取到任務鎖,並執行了任務
線程:[9]獲取到任務鎖,並執行了任務
線程:[6]獲取到任務鎖,並執行了任務
線程:[5]獲取到任務鎖,並執行了任務
線程:[18]獲取到任務鎖,並執行了任務
線程:[7]獲取到任務鎖,並執行了任務
線程:[13]獲取到任務鎖,並執行了任務

方案二優劣

優點

  • 實現對父節點變動狀態(主要是子節點列表變化)的監聽
  • 當子節點列表出現變化後,ZK通知監聽的各個線程,各個線程查詢子節點狀態
  • 相對於輪詢方式來說,避免了很大一部分網絡開銷和資源浪費
  • 對父節點進行監聽,實現起來相對簡單

劣勢

  • 每個在阻塞狀態下競爭鎖的線程,都監聽了父節點狀態,即父節點出現變動(主要是子節點列表變化)後,ZK服務器需要通知到所有註冊監聽的線程,網絡消耗和資源浪費依然比較大

方案三 :子節點監聽方式

實現原理

程序流程圖如下:

這裏寫圖片描述

實現代碼

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Created by xuyh at 2017/11/29 19:00.
 * <p>
 * **最優方案**
 * <pre>
 *     方案三:子節點監聽方式
 *
 *     實現思路:監聽子節點狀態
 *     1.在父節點(持久化)下創建臨時節點,實際創建的節點路徑會根據數量進行自增(ZK自編號方式創建節點)。
 *     2.創建節點成功後,首先獲取父節點下的子節點列表,判斷本線程的路徑後綴編號是否是所有子節點中最小的,若是則獲取鎖,反之監聽本節點前一個節點(路徑排序爲本節點路徑數字減一的節點)變動狀態(通過getData()方法註冊watcher)
 *     3.當監聽對象狀態變動(節點刪除狀態)後watcher會接收到通知,這時再次判斷父節點下的子節點的排序狀態,若滿足本線程的路徑後綴編號最小則獲取鎖,反之繼續註冊watcher監聽前一個節點狀態
 * </pre>
 */
public class ZKLockThree {
    private Logger logger = LoggerFactory.getLogger(ZKLockThree.class);
    private static final String CHILD_NODE_PATH = "temp";
    private String baseLockPath;
    private String finalLockId;

    //是否需要中斷阻塞標誌位
    private boolean needInterrupt = false;
    //ZK是否連接成功標誌位
    private boolean connected = false;
    //是否獲取到鎖標誌位
    private boolean acquireLock = false;

    private String host = "127.0.0.1";
    private String port = "2181";
    private ZooKeeper zooKeeper;
    private PreviousNodeWatcher previousNodeWatcher;

    private ZKLockThree(String host, String port, String lock) {
        this.host = host;
        this.port = port;
        this.baseLockPath = "/" + lock;
        this.previousNodeWatcher = new PreviousNodeWatcher(this);
    }

    /**
     * 新建鎖(連接ZK阻塞)
     *
     * @param host zk 服務ip
     * @param port zk 服務端口
     * @param lock 鎖名稱
     * @return
     */
    public static ZKLockThree create(String host, String port, String lock) {
        ZKLockThree zkLockThree = new ZKLockThree(host, port, lock);
        zkLockThree.connectZooKeeper();
        return zkLockThree;
    }

    /**
     * 獲取鎖(阻塞)
     *
     * @return true代表獲取到分佈式任務鎖
     */
    public boolean getLock() {
        if (!connected)
            return false;
        while (!needInterrupt) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                logger.warn(e.getMessage(), e);
            }

            if (acquireLock) {
                return true;
            }
        }
        return false;
    }

    /**
     * 釋放鎖
     *
     * @return true代表釋放鎖成功, 並切斷ZK連接
     */
    public boolean releaseLock() {
        try {
            if (zooKeeper != null && connected) {
                zooKeeper.delete(finalLockId, -1);
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
        return disconnectZooKeeper();
    }

    private boolean disconnectZooKeeper() {
        if (zooKeeper == null && !connected)
            return false;
        try {
            connected = false;
            acquireLock = false;
            zooKeeper.close();
        } catch (Exception e) {
            logger.warn(String.format("ZK disconnect failed. [%s]", e.getMessage()), e);
        }
        return true;
    }

    private boolean connectZooKeeper() {
        try {
            //連接ZK
            zooKeeper = new ZooKeeper(host + ":" + port, 60000, event -> {
                if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                    needInterrupt = true;
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                    needInterrupt = true;
                } else {
                    if (event.getType() == Watcher.Event.EventType.None) {//連接成功
                        connected = true;
                    }
                }
            });

            //等待異步連接成功,超過時間30s則退出等待,防止線程鎖死
            int i = 1;
            while (!connected) {
                if (i == 100)
                    break;
                Thread.sleep(300);
                i++;
            }

            if (connected) {
                //創建父節點
                if (zooKeeper.exists(baseLockPath, false) == null) {
                    zooKeeper.create(baseLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }

                //創建子節點
                finalLockId = zooKeeper.create(baseLockPath + "/" + CHILD_NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.EPHEMERAL_SEQUENTIAL);

                //檢查一次是否獲取到鎖
                checkAcquire();
            } else {
                needInterrupt = true;
                logger.warn("Connect zookeeper failed. Time consumes 30 s");
                return false;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    private void checkAcquire() {
        if (!connected)
            return;
        try {
            //獲取子節點列表,若沒有獲取到鎖,註冊監聽,監聽對象應當是比本節點路徑編號小一(或者排在前面一位)的節點
            List<String> childrenList = zooKeeper.getChildren(baseLockPath, false);

            if (judgePathNumMin(childrenList)) {
                acquireLock = true;//獲取到鎖
            } else {
                watchPreviousNode(childrenList);
            }

        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            disconnectZooKeeper();
        }
    }

    private boolean judgePathNumMin(List<String> paths) {
        if (paths.isEmpty())
            return true;
        if (paths.size() >= 2) {
            //對無序狀態的子節點路徑列表按照編號升序排序
            paths.sort((str1, str2) -> {
                int num1;
                int num2;
                String string1 = str1.substring(CHILD_NODE_PATH.length(), str1.length());
                String string2 = str2.substring(CHILD_NODE_PATH.length(), str2.length());
                num1 = Integer.parseInt(string1);
                num2 = Integer.parseInt(string2);
                if (num1 > num2) {
                    return 1;
                } else if (num1 < num2) {
                    return -1;
                } else {
                    return 0;
                }
            });
        }

        //判斷本線程的節點路徑是否是最小編號
        String minId = paths.get(0);
        return finalLockId.equals(baseLockPath + "/" + minId);
    }

    private void watchPreviousNode(List<String> paths) {
        if (paths.isEmpty() || paths.size() == 1) {
            needInterrupt = true;
            return;
        }
        int currentNodeIndex = paths.indexOf(finalLockId.substring((baseLockPath + "/").length(), finalLockId.length()));
        String previousNodePath = baseLockPath + "/" + paths.get(currentNodeIndex - 1);
        //通過getData方法註冊watcher
        try {
            zooKeeper.getData(previousNodePath, previousNodeWatcher, new Stat());
        } catch (Exception e) {
            //watcher註冊失敗,退出鎖競爭
            logger.warn(String.format("Previous node watcher register failed! message: [%s]", e.getMessage()), e);
            needInterrupt = true;
        }
    }

    private class PreviousNodeWatcher implements Watcher {
        private ZKLockThree context;

        PreviousNodeWatcher(ZKLockThree context) {
            this.context = context;
        }

        @Override
        public void process(WatchedEvent event) {
            if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                context.needInterrupt = true;
            } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                context.needInterrupt = true;
            } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                context.needInterrupt = true;
            } else {
                //節點被刪除了,說明這個節點釋放了鎖
                if (event.getType() == Event.EventType.NodeDeleted) {
                    context.checkAcquire();
                }
            }
        }
    }
}


測試

測試程序

private void testZKLockThreeWithMultiThread() throws Exception {
    int threadCount = 20;
    List<TestLockThreeThread> testLockThreeThreads = new ArrayList<>();
    for (int i = 0; i < threadCount; i++) {
        testLockThreeThreads.add(new TestLockThreeThread("" + i, "127.0.0.1", "2181", "TestLockThreeThreadLock"));
    }
    testLockThreeThreads.forEach(TestLockThreeThread::start);
    Thread.sleep(100000);
}

private class TestLockThreeThread extends Thread {
    private String host;
    private String port;
    private String lockPath;

    private String num;

    /**
     * @param threadNum 線程編號
     */
    public TestLockThreeThread(String threadNum, String host, String port, String lockPath) {
        this.host = host;
        this.port = port;
        this.lockPath = lockPath;
        this.num = threadNum;
    }

    @Override
    public void run() {
        ZKLockThree zkLockThree = ZKLockThree.create(host, port, lockPath);
        if (zkLockThree.getLock()) {
            System.out.println(String.format("線程:[%s]獲取到任務鎖,並執行了任務", num));
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
        } else {
            System.out.println(String.format("線程:[%s]沒有獲取到任務鎖,放棄執行任務", num));
        }
        zkLockThree.releaseLock();
    }
}

結果:

線程:[4]獲取到任務鎖,並執行了任務
線程:[7]獲取到任務鎖,並執行了任務
線程:[2]獲取到任務鎖,並執行了任務
線程:[9]獲取到任務鎖,並執行了任務
線程:[8]獲取到任務鎖,並執行了任務
線程:[6]獲取到任務鎖,並執行了任務
線程:[3]獲取到任務鎖,並執行了任務
線程:[19]獲取到任務鎖,並執行了任務
線程:[12]獲取到任務鎖,並執行了任務
線程:[15]獲取到任務鎖,並執行了任務
線程:[14]獲取到任務鎖,並執行了任務
線程:[16]獲取到任務鎖,並執行了任務
線程:[17]獲取到任務鎖,並執行了任務
線程:[18]獲取到任務鎖,並執行了任務
線程:[11]獲取到任務鎖,並執行了任務
線程:[13]獲取到任務鎖,並執行了任務
線程:[0]獲取到任務鎖,並執行了任務
線程:[1]獲取到任務鎖,並執行了任務
線程:[5]獲取到任務鎖,並執行了任務
線程:[10]獲取到任務鎖,並執行了任務

方案優劣

優點

  • 實現對子節點變動狀態(排序在本線程對應節點之前的一個節點)的監聽
  • 被監聽子節點變動(刪除)之後,ZK通知本線程執行相應操作,進行鎖競爭
  • 相對於父節點監聽方式來說,子節點監聽方式在每一次鎖釋放(或者節點變動)時,ZK僅通知到一個線程的watcher操作,節省了大量的網絡消耗和資源佔用

劣勢

  • 實現方式與程序邏輯較輪詢和父節點監聽來說比較繁瑣

總結比較

對這三種基於ZK的分佈式任務鎖的實現方式進行比較,可以得出這些結論:

  • 程序複雜度:
    輪詢方式 < 父節點監聽方式 < 子節點監聽方式

  • 網絡資源消耗:
    輪詢方式 >> 父節點監聽方式 >> 子節點監聽方式

  • 程序可靠性
    輪詢方式 << 父節點監聽方式 < 子節點監聽方式

輕量Zookeeper客戶端實現

github地址:

https://github.com/johnsonmoon/zk-client

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章