六、分布式锁的实现原理

实现分布式锁的方式有许多种,比如使用数据库锁、redis等等。而Zookeeper也可以用于实现分布式锁,下面通过源码来介绍Zookeeper是如何来实现分布式锁的?


6.1 惊群效应

假设用节点"/lock"来表示这个分布式锁,我们第一时间可能会想到通过判断节点"/lock"存在与否,如果不存在则创建该节点,表示获取到这个锁,否则监听该节点,等待锁的释放。如下图所示
image.png

这种方案在客户端数据较多的时候,所有客户端都监听该"/lock"节点,而当"/lock"节点移除时,zk服务需要做大量的通知处理,导致zk服务的性能骤降。这种现象就叫惊群效应(网上很多地方翻译成羊群效应,个人感觉惊群效应更贴切一些)


6.2 实现原理

在Zookeeper的源码中已经有分布式锁的实现,在zookeeper-recipes/zookeeper-recipes-lock目录下。在节点"/lock"下的子节点都是EPHEMERAL_SEQUENTIAL临时顺序节点,第一个子节点为持有当前分布式锁的线程,后续的子节点监听前一个子节点,这样就解决了惊群效应的问题

其实现的示意图如下:
image.png

大致步骤可以分为以下几步:

  • 判断/lock下面是否有子节点
  • 如果没有,说明当前分布式锁未被持有,则创建临时顺序子节点,节点名称为x-sessionId-序列号
  • 如果有,说明当前分布式锁已被持有,在该目录下创建临时顺序节点,并监听它的上一个子节点
  • 当第一个子节点释放锁,第二个节点监听到后,重新调用lock()方法判断当前是不是拿到了锁。如此反复

6.3 源码解读

调用lock()方法来获取分布式锁,源码如下

public synchronized boolean lock() throws KeeperException, InterruptedException {
        if (isClosed()) {
            return false;
        }
        // 确保路径存在,不存在则创建
        ensurePathExists(dir);
        
        // 尝试获取锁
        return (Boolean) retryOperation(zop);
    }

底层调用LockZooKeeperOperation类的execute()方法,尝试获取锁,如果拿不到,则监听上一个子节点,代码如下

public boolean execute() throws KeeperException, InterruptedException {
    do {
        if (id == null) {
            long sessionId = zookeeper.getSessionId();
            String prefix = "x-" + sessionId + "-";
            // 判断该会话是否之前已经有创建子节点,如果有,则拿之前创建好的id名,否则新建子节点
            findPrefixInChildren(prefix, zookeeper, dir);
            idName = new ZNodeName(id);
        }
        List<String> names = zookeeper.getChildren(dir, false);
        if (names.isEmpty()) {
            LOG.warn("No children in: {} when we've just created one! Lets recreate it...", dir);
            // 重建节点
            id = null;
        } else {
            // 对这些子节点id进行排序,并拿到比当前子节点id小的列表
            SortedSet<ZNodeName> sortedNames = new TreeSet<>();
            for (String name : names) {
                sortedNames.add(new ZNodeName(dir + "/" + name));
            }
            ownerId = sortedNames.first().getName();
            SortedSet<ZNodeName> lessThanMe = sortedNames.headSet(idName);

            // 如果比当前子节点id小的列表不为空,则监听上一个子节点
            if (!lessThanMe.isEmpty()) {
                ZNodeName lastChildName = lessThanMe.last();
                lastChildId = lastChildName.getName();
                LOG.debug("Watching less than me node: {}", lastChildId);
                Stat stat = zookeeper.exists(lastChildId, new LockWatcher());
                if (stat != null) {
                    return Boolean.FALSE;
                } else {
                    LOG.warn("Could not find the stats for less than me: {}", lastChildName.getName());
                }
            } else {
                // 否则判断当前子节点id是不是最小id,如果是,则返回true,表示获取到锁
                if (isOwner()) {
                    LockListener lockListener = getLockListener();
                    if (lockListener != null) {
                        lockListener.lockAcquired();
                    }
                    return Boolean.TRUE;
                }
            }
        }
    }
    while (id == null);
    return Boolean.FALSE;
}

当客户端释放锁时,调用unlock()方法,删除当前对应id的子节点

public synchronized void unlock() throws RuntimeException {
    if (!isClosed() && id != null) {
        try {
            ZooKeeperOperation zopdel = () -> {
                // 删除当前对应id的子节点
                zookeeper.delete(id, -1);
                return Boolean.TRUE;
            };
            zopdel.execute();
        } catch (InterruptedException e) {
            // 略
        } finally {
            LockListener lockListener = getLockListener();
            if (lockListener != null) {
                lockListener.lockReleased();
            }
            id = null;
        }
    }
}

kamier
1.5k 声望493 粉丝