Zookeeper怎样实现分布式锁?

130 阅读4分钟

src=http___static0.f.dajiangtai.com_www_images_c_33.jpg&refer=http___static0.f.dajiangtai.webp
本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

       在《Redis怎样实现分布式锁?》《Redis怎样实现可重入分布式锁?》中,我们了解到了为什么需要分布式锁,并且学会了怎样用Redis实现分布式锁。不过分布式锁除了可以使用Redis实现以外,还可以使用数据库或者Zookeeper实现,本文将为大家讲解怎样用Zookeeper实现分布式锁。

Zookeeper节点类型

       在Zookeeper中,节点分为多种类型,具体如下:

  • 持久节点
           节点创建后,不管是否跟客户端断开连接,节点都会一直存在。
  • 临时节点
           节点创建后,只要与客户端断开连接,节点就会消失。
  • 顺序节点
           节点创建后,Zookeeper会根据创建的时间顺序,自动在节点名称后面增加上类似0000001的数字表示顺序。
           顺序节点既可以是持久节点,也可以是临时节点。

Zookeeper监听机制

       Zookeeper提供监听机制,用来支持客户端进行相应状态或者事件监听,目前支持的事件或者状态监听如下:

  1. 通知状态
    • SyncConnected:客户端与服务器正常连接时通知
    • Disconnected:客户端与服务器断开连接时通知
    • Expired:会话Session过期时通知
    • AuthFailed:身份认证失败时通知
  2. 事件类型(基于SyncConnected状态)
    • None:无
    • NodeCreated:监听的节点被创建时通知
    • NodeDeleted:监听的节点被删除时通知
    • NodeDataChanged:监听的节点数据变更时通知
    • NodeChildrenChanged:监听的节点的子节点列表发生变更时通知

Zookeeper分布式锁实现逻辑

       Zookeeper分布式锁的实现就是基于临时顺序节点节点删除通知来实现的。

加锁

       加锁时,我们首先根据锁标识,在指定的锁根目录下创建一个持久化节点,即为锁节点,代码如下:

        // 锁节点路径
        String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
        try {
            // 创建锁节点
            zooKeeper.create(lockParentPath,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        } catch (Exception e) {
            logger.error("创建锁节点异常,lockParentPath:{}", lockParentPath, e);
        }

       接下来,我们通过在创建好的锁节点下面创建临时顺序子节点,如果当前线程创建好的临时顺序子节点的序号是最小的,则表示获取到锁。代码如下:

            this.lockPath = zooKeeper.create(lockParentPath+"lock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 校验是否是最小节点,是则获取成功,不是则监听前一个节点
            if (getLockAndWatchLast()) {
                logger.info("加锁成功,lockKey:{}", lockKey);
            }

       若当前线程获取到的临时顺序子节点的序号不是最小的,则需要监听其前一个节点,在前一个节点被删除时,重新校验当前线程的临时顺序子节点的序号是不是最小的,如果是,则获取锁成功。代码如下:

        CountDownLatch latch = new CountDownLatch(1);
        // 获取前一个节点
        String prePath = getPrePath(children, curNodeName);
        // 监听prePath是否存在
        zk.exists(prePath, new Watcher() {
            public void process(WatchedEvent event) {
                // prePath不存在
                latch.countDown();
            }
        });
        
        // 闭锁等待
        latch.await();
        // 重新校验并获取锁
        getLockAndWatchLast();

释放锁

       删除当前线程获取到的锁临时节点即可。如果当前锁已经没有子节点,则表示当前没有其它线程竞争锁,则把锁对应的路径也进行删除。

Zookeeper分布式锁完整代码

public class ZookeeperDistributedLock implements Lock {
    /**线程本地存储,存储锁的密钥和计数:结构为 锁id:加锁计数*/
    private static ThreadLocal<Map<String, Integer>> threadLocal = new ThreadLocal<>();
    // 锁根目录
    private String LOCK_ROOT_PATH="/test/locks";
    // 锁键值
    private String              lockKey;
    // Zookeeper客户端
    private ZooKeeper zooKeeper;
    // 当前线程获取到的锁路径
    private String lockPath;
    
    public ZookeeperDistributedLock(String lockKey, ZooKeeper zooKeeper) {
        this.lockKey = lockKey;
        this.zooKeeper = zooKeeper;
    }
    
    /**
     * 尝试获取锁
     */
    @Override
    public boolean tryLock() {
        // 当前线程持有的锁及其计数
        Map<String,Integer> value = threadLocal.get();
        if(value.containsKey(lockKey)){
            value.put(lockKey,value.get(lockKey) + 1);
            return true;
        }
        
        // 获取锁成功
        if (getLock()) {
            value.put(lockKey,1);
            return true;
        }
        return false;
    }
    
    /**
     * 获取锁
     */
    private boolean getLock() {
        // 锁节点路径
        String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
        try {
            // 创建锁节点
            zooKeeper.create(lockParentPath,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            // 执行加锁
            return doLock(lockParentPath);
        } catch (Exception e) {
            logger.error("创建锁节点异常,lockParentPath:{}", lockParentPath, e);
        }
        return false;
    }
    
    // 执行加锁
    private boolean doLock(String lockParentPath) {
        try {
            this.lockPath = zooKeeper.create(lockParentPath+"lock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 校验是否是最小节点,是则获取成功,不是则监听前一个节点
            if (getLockAndWatchLast()) {
                logger.info("加锁成功,lockKey:{}", lockKey);
            }
        } catch(Exception e) {
            logger.error("执行加锁异常,lockParentPath:{}", lockParentPath, e);
        }
        return false;
    }
    
    // 校验是否是最小节点,是则获取成功,不是则监听前一个节点
    private boolean getLockAndWatchLast() {
        List<String> children = zooKeeper.getChildren(lockParentPath, false);
        Collections.sort(children);
        String[] paths=this.lockPath.split("/");
        String curNodeName =  paths[paths.length-1];
        if (children.get(0).equalsIgnoreCase(curNodeName)) {
            return true;
        }
        
        CountDownLatch latch = new CountDownLatch(1);
        // 获取前一个节点
        String prePath = getPrePath(children, curNodeName);
        // 监听prePath是否存在
        zk.exists(prePath, new Watcher() {
            public void process(WatchedEvent event) {
                // prePath不存在
                latch.countDown();
            }
        });
        
        // 闭锁等待
        latch.await();
        // 重新校验并获取锁
        getLockAndWatchLast();
    }
    
    // 获取前一个节点
    private String getPrePath(List<String> children, String curNodeName) {
        for (int i = 0; i < children.size(); i++) {
            String child = children.get(i);
            if (child.equalsIgnoreCase(curNodeName)) {
                break;
            }
        }
        return children.get(i - 1);
    }
    
    /**
     * 释放锁
     */
    @Override
    public void unlock(String lockKey,String requestId) {
        // 当前线程持有的锁及其计数
        Map<String,Integer> value = threadLocal.get();
        if(value.containsKey(lockKey)){
            Integer num = value.get(lockKey) - 1;
            // 锁计数小于等于0,需要调用Zookeeper进行锁释放
            if(num <= 0){
                // 释放锁
                if (doUnLock()) {
                    logger.info("释放锁成功,lockKey:{}", lockKey);
                    value.remove(lockKey);
                } else {// 失败
                    logger.info("释放锁失败,lockKey:{}", lockKey);
                }
            } else {
                // 减少锁计数
                value.put(lockKey,num);
            }
        } else {
            logger.info("释放锁成功,当前线程未持有锁,lockKey:{}", lockKey);
        }
    }
    
    // 释放锁
    private boolean doUnLock() {
        try {
            if (this.lockPath != null) {
                // 删除节点,释放锁
                zooKeeper.delete(this.lockPath, -1);
            }
            
            // 锁节点路径
            String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
            List<String> children = zooKeeper.getChildren(lockParentPath, false);
            if (children.isEmpty()) {
                try {
                    // 删除锁节点
                    zooKeeper.delete(lockParentPath, -1);
                } catch (KeeperException e) {
                    logger.error("删除锁节点异常,lockParentPath:{}", lockParentPath, e);
                }
            }
            
            // 关闭Zookeeper连接
            if (zooKeeper != null) {
                zooKeeper.close();
            }
        } catch(Exception e) {
            logger.error("释放锁异常,lockParentPath:{}", lockParentPath, e);
        }
        return false;
    }
}

后言

       既然看到这里了,感觉有所收获的朋友,不妨来个大大的点赞吧~~~