Zookeeper(5)---分散式鎖

白露非霜發表於2020-12-05

基於臨時序號節點來實現分散式鎖

為什麼要用臨時節點呢?如果拿到鎖的服務當機了,會話失效ZK自己也會刪除掉臨時的序號節點,這樣也不會阻塞其他服務。

 

流程:

1.在一個持久節點下面建立臨時的序號節點作為鎖節點,如:/lock/lockId00000001 /lock/lockId00000002

2.獲取持久節點下面所有子節點,判斷其最小的是不是自己,如果是自己則表示獲取鎖成功。

3.如果最小的結點不是自己,則阻塞等待,並對lock結點新增監聽,如果結點數發生變化了,則說明有釋放了鎖。但是這裡如果存在大量監聽,當結點發生變化的時候,可能就會出現羊群效應。因為大家都監聽了,同時會通知很多客戶端,會造成ZK效能突然下降。

所以這裡可以只監聽自己前面的那個節點,排隊執行,03監聽02,02監聽01

4.當鎖釋放,節點監聽到變化之後,執行第2步。

釋放鎖只需要刪除對應的臨時節點,如果獲取到鎖的服務當機了,因為是臨時節點也會自己刪除的。

程式碼實現:

package com.nijunyang.zookeeper.zklock;

import lombok.Data;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Description:
 * Created by nijunyang on 2020/11/29 21:51
 */
public class ZKLock {
    private static final String SERVER = "192.168.0.67:2181";
    private ZkClient zkClient;
    private static final String ROOT_PATH = "/lock";

    public ZKLock() {
        zkClient = new ZkClient(SERVER, 5000, 20000);
        buildRoot();
    }

    private void buildRoot() {
        if (!zkClient.exists(ROOT_PATH)) {
            zkClient.createPersistent(ROOT_PATH);
        }
    }

    /**
     * 加鎖
     *
     * @param lockId
     * @param timeout
     * @return
     */
    public Lock lock(String lockId, long timeout) {
        Lock lockNode = createLock(lockId);
        // 嘗試啟用鎖
        tryActiveLock(lockNode);
        if (!lockNode.isActive()) {
            try {
                synchronized (lockNode) {
                    lockNode.wait(timeout);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        if (!lockNode.isActive()) {
            throw new RuntimeException("獲取鎖超時");
        }
        return lockNode;
    }

    /**
     * 釋放鎖
     *
     * @param lock
     */
    public void unlock(Lock lock) {
        if (lock.isActive()) {
            zkClient.delete(lock.getPath());
        }
    }

    /**
     * 啟用鎖
     * @param lockNode
     */
    private void tryActiveLock(Lock lockNode) {
        // 判斷當前是否為最小節點
        List<String> list = zkClient.getChildren(ROOT_PATH)
                .stream()
                .sorted()
                .map(p -> ROOT_PATH + "/" + p)
                .collect(Collectors.toList());
        String firstNodePath = list.get(0);

        //第一個節點是當前節點,將鎖設定為啟用
        if (firstNodePath.equals(lockNode.getPath())) {
            lockNode.setActive(true);
        } else {
            //第一個節點不是當前節點,則監聽前面一個節點
            String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
            zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
                @Override
                public void handleDataChange(String dataPath, Object data) throws Exception {
                }
                @Override
                public void handleDataDeleted(String dataPath) throws Exception {
                    //監聽之後繼續嘗試加鎖,加鎖成功喚醒之前的等待
                    tryActiveLock(lockNode);
                    synchronized (lockNode) {
                        if (lockNode.isActive()) {
                            lockNode.notify();
                        }
                    }
                    zkClient.unsubscribeDataChanges(upNodePath, this);
                }
            });
        }
    }
    /**
     * 建立鎖節點
     *
     * @param lockId
     * @return Lock
     */
    private Lock createLock(String lockId) {
        String nodePath = zkClient.createEphemeralSequential(ROOT_PATH + "/" + lockId, "write");
        return new Lock(lockId, nodePath);
    }

    @Data
    public static class Lock {

        private String lockId;
        private String path;
        /**
         * 是否啟用鎖
         */
        private boolean active;

        public Lock(String lockId, String path) {
            this.lockId = lockId;
            this.path = path;
        }
    }
}

 

相關文章