聊聊分布式锁的实现(二)

833 阅读7分钟

上一篇给大家介绍了基于redis的分布式锁不知道有没有给你解释清楚,这次介绍一种基于zooKeeper的实现方式,本文只会介绍相关的zooKeeper知识,有兴趣的同学可以自行学习。

基于zooKeeper实现的分布式锁

一、相关概念

zookeeper的知识点在这里就不详细介绍了,下面列出一些跟实现分布式锁相关的概念

  • 临时节点:临时节点区别于持久节点的就是它只存在于会话期间,会话结束或者超时会被自动删除;
  • 有序节点:顾名思义就是有顺序的节点,zookeeper会根据现有节点做一个序号顺延,如第一个创建的节点是/xiamu/lock-00001,下一个节点就是/xiamu/lock-00002;
  • 监听器:监听器的作用就是监听一些事件的发生,比如节点数据变化、节点的子节点变化、节点的删除;

二、临时节点方案

基于zookeeper的临时节点方案,主要利用了zookeeper的创建节点的原子性、临时节点、监听器等功能,大致上的思路如下:

  1. 客户端加锁时创建一个临时节点,创建成功则加锁成功。
  2. 加锁失败则创建一个监听器用于监听这个节点的变化,然后当前线程进入等待。
  3. 持有锁的客户端解锁时会删除这个节点,或者会话结束自动被删除。
  4. 监听器监听到节点的删除通知等待的客户端去重新获取锁。

图一:临时节点实现分布式锁

部分代码实现如下:

/**
* 加锁代码实现
**/
 public void lock(String path) throws Exception {
    boolean hasLock = false;
    while (!hasLock) {
        try {
            this.createTemporaryNode(path, "data");
            hasLock = true;
            log.info("{}获取锁成功", Thread.currentThread().getName());
        } catch (Exception e) {
            synchronized (this) {
                try {
                    zooKeeperClient.getData(path, event -> {
                        if (SyncConnected.equals(event.getState()) && NodeDeleted.equals(event.getType())) {
                            notifyWait();
                        }
                    }, null);
                    wait();
                } catch (KeeperException.NoNodeException ex) {
                    log.info("节点已不存在");
                }
            }
        }
    }
}

/**
* 唤醒等待锁的线程
**/
public synchronized void notifyWait() {
    notifyAll();
}

这里我是使用的是ZooKeeper的Java原生API实现,这段实现代码并不严谨,我只是为了为了描述相关逻辑;ZooKeeper的Java原生API存在一些问题如:客户端断开连接时需要手动去重新连接;监听器只能使用一次,想要继续使用需要重复注册;上述代码实现中如果监听器被节点的数据改变事件触发了,那么就无法再一次监听节点删除事件。推荐大家使用第三方开源框架Curator

三、临时顺序节点方案

临时顺序节点方案和上述方案的不同点在于:

  1. 这里所有的客户端都能创建临时顺序节点,只有加锁路径下第一个节点才能获取锁;
  2. 获取锁失败的客户端并不监听获取锁的客户端的节点,而是监听自己的前一个节点;
  3. 具有可重入性;

在这里我们使用Curator已有的轮子来实现这个方案,并跟着源码来分析一下主要思路

InterProcessMutex lock = curatorLock.getCuratorLock(path);

/**
 * curator获取锁
 */
public InterProcessMutex getCuratorLock(String path) {
    return new InterProcessMutex(curatorClient, path);
}
    
/**
 * curator方式加锁
 * @param lock 锁
 */
public void curatorLock(InterProcessMutex lock) {
    try {
        lock.acquire();
        log.info("{}获取锁成功", Thread.currentThread().getName());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * curator方式释放锁
 * @param lock 锁
 */
public void curatorReleaseLock(InterProcessMutex lock) {
    if (null != lock && lock.isAcquiredInThisProcess()) {
        try {
            lock.release();
            log.info("{}释放锁成功", Thread.currentThread().getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这种实现是不是非常方便呢,实际上主要逻辑就是之前讲过的那些,都封装在内部了,这里简单调用一下API即可实现。下面我们来看看acquire()和release()方法的源码分析实现方式。

源码分析

// 首先我们看acquire()方法的对象InterProcessMutex
// 从它的构造方法我们看下来可以得知这个锁的基础路径就是我们传入的path,锁的名字暂时是lock-开头
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver) {
    this(client, path, LOCK_NAME(lock-), 1, driver);
}

InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver) {
    basePath = PathUtils.validatePath(path);
    internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
@Override
public void acquire() throws Exception {
    if ( !internalLock(-1, null) ) {
        throw new IOException("Lost connection while trying to acquire lock: " + basePath);
    }
}

private boolean internalLock(long time, TimeUnit unit) throws Exception {
    // 获取当前加锁线程
    Thread currentThread = Thread.currentThread();
    // 从一个ConcurrentMap缓存中尝试获取当前线程信息
    LockData lockData = threadData.get(currentThread);
    // 如果map中存在这个线程则说明当前线程已加锁成功,加锁次数加一,返回加锁成功
    if ( lockData != null ) {
        lockData.lockCount.incrementAndGet();
        return true;
    }
    // 尝试加锁并返回加锁路径
    String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
    // 加锁成功
    if ( lockPath != null ) {
        LockData newLockData = new LockData(currentThread, lockPath);
        // 构造一个加锁数据并加入缓存map
        threadData.put(currentThread, newLockData);
        return true;
    }
    return false;
}

private static class LockData {
    // 当前加锁线程
    final Thread owningThread;
    // 加锁path
    final String lockPath;
    // 加锁次数
    final AtomicInteger lockCount = new AtomicInteger(1);
}

String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
    final long      startMillis = System.currentTimeMillis();
    final Long      millisToWait = (unit != null) ? unit.toMillis(time) : null;
    final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
    int             retryCount = 0;
    String          ourPath = null;
    boolean         hasTheLock = false;
    boolean         isDone = false;
    while ( !isDone )
    {
        isDone = true;
        try {
            // 在加锁路径下创建临时顺序节点并返回路径
            ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
            // 获取锁
            hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
        }
        catch ( KeeperException.NoNodeException e ) {
            // 会话超时会导致找不到锁定节点,重新尝试连接(允许重试)
            if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) {
                // 连接成功重新尝试加锁
                isDone = false;
            } else {
                // 连接失败抛出异常
                throw e;
            }
        }
    }
    // 获取锁成功返回加锁路径
    if ( hasTheLock ) {
        return ourPath;
    }
    return null;
}

// 在加锁路径下创建一个临时顺序节点并返回路径
@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
    String ourPath;
    if ( lockNodeBytes != null ) {
        ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
    } else {
        ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
    }
    return ourPath;
}

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
    boolean     haveTheLock = false;
    boolean     doDelete = false;
    try {
        if ( revocable.get() != null ) {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }
        // 客户端已启动并且没有获取锁则循环重试
        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
            // 获取从小到大排序的加锁路径下的子节点列表
            List<String> children = getSortedChildren();
            // 获取当前节点的序列号
            String sequenceNodeName = ourPath.substring(basePath.length() + 1); 
            // 判断是否能获取锁,返回是否成功和需要监听的路径
            PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() ) {
                haveTheLock = true;
            } else {
                // 需要监听节点的完整路径
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
                synchronized(this)
                {
                    try {
                        // 监听节点                       
                        client.getData().usingWatcher(watcher).forPath(previousSequencePath);
                        // 设置了超时等待时间
                        if ( millisToWait != null ) {
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            // 等待时间到了退出获取
                            if ( millisToWait <= 0 ) {
                                doDelete = true;    // timed out - delete our node
                                break;
                            }
                            // 超时等待
                            wait(millisToWait);
                        } else {
                            // 进入等待
                            wait();
                        }
                    } catch ( KeeperException.NoNodeException e ) {
                        // it has been deleted (i.e. lock released). Try to acquire again
                    }
                }
            }
        }
    }
    catch ( Exception e ) {
        ThreadUtils.checkInterrupted(e);
        doDelete = true;
        throw e;
    } finally {
        if ( doDelete )  {
            // 等待时间到了没有获取锁或则抛出异常则删除自己的节点
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}

@Override
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
    // 获取当前的index
    int ourIndex = children.indexOf(sequenceNodeName);
    validateOurIndex(sequenceNodeName, ourIndex);
    // 如果当前的index比上一个小则获得锁
    boolean getsTheLock = ourIndex < maxLeases;
    // 如果没有获得锁则获取前一个节点的路径
    String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
    return new PredicateResults(pathToWatch, getsTheLock);
}
// 监听器,事件触发时唤醒等待的线程
private final Watcher watcher = new Watcher() {
    @Override
    public void process(WatchedEvent event) {
        notifyFromWatcher();
    }
};

@Override
public void release() throws Exception {
    Thread currentThread = Thread.currentThread();
    // 根据当前线程从缓存map中获取加锁信息
    LockData lockData = threadData.get(currentThread);
    // 如果没有信息则说明没有获得到锁
    if ( lockData == null ) {
        throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
    }
    // 减少加锁次数一
    int newLockCount = lockData.lockCount.decrementAndGet();
    // 如果还有加锁次数则返回
    if ( newLockCount > 0 ) {
        return;
    }
    if ( newLockCount < 0 ) {
        throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
    }
    try {
        // 释放锁
        internals.releaseLock(lockData.lockPath);
    } finally {
        // 从缓存map中移除加锁信息
        threadData.remove(currentThread);
    }
}

final void releaseLock(String lockPath) throws Exception {
    // 移除监听
    client.removeWatchers();
    revocable.set(null);
    // 删除节点
    deleteOurPath(lockPath);
}

整体流程:加锁时对某个路径创建临时顺序节点,如果当前已经获取了锁,那么加锁次数加一;否则如果创建的临时节点是当前路径下第一个节点那么加锁成功;否则找到当前加锁路径下的子节点列表,找到自己的前一个节点并监听然后进入等待,如果前一个节点释放了锁或者当前会话失效那么节点删除触发监听事件,注册监听的线程唤醒重新获取锁。

总结

其实分布式锁还存在着其他的问题,我这两篇文章中没有讲到;其中一个是获取锁的线程进入等待释放了锁唤醒之后如何保证不会重复执行由期间获取锁的线程执行的操作。如果你如果你的应用只需要高性能的分布式锁不要求多高的正确性,那么单节点 Redis 够了;如果你的应用想要保住正确性,那么不建议使用集群模式下的 Redlock算法,建议使用ZooKeeper且保证存在fencing token(即上述问题的解决方案递增版本号)。