利用zookeeper手动实现分布式锁

前言

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

对于分布式锁,想必大家都不陌生。分布式锁的作用和Synchronized的作用一样,都是限制共享资源的访问。只不过Synchronized只能作用於单机,而分布式锁作用于分布式系统。

想要实现一个简单的锁,其实只需实现两个部分:队列同步器。同步器用来控制任务(task)和线程的绑定与解绑,队列用来存放暂未抢到锁的线程。简单来说,就是怎么处理抢到锁的线程,和怎么处理没抢到锁的线程。这不仅仅适用於单机锁,也同样适用于分布式锁。当然,如果想要实现可重入可中断公平非公平以及类似wait/notify等功能,那就相对复杂了。

前置阅读

这两篇分别介绍了zookeeper集群的搭建,以及zookeeper响应式API介绍,需要的自取。

zookeeper

为什么zookeeper适合做分布式锁?

  • zookeeper的Znode节点类似Linux系统的文件目录,且同一目录下Znode名称唯一,这个唯一性就相当于天然支持了锁
  • zookeeper支持临时节点,session断开后会自动删除临时节点,所以即使加锁的线程中途异常,也不会发生死锁
  • zookeeper支持watch机制,能及时通知各个事件
  • zookeeper自带高可用,稳定压倒一切
  • zookeeper性能强悍(相比之下)

想要利用zookeeper实现一个分布式锁,首先要解决哪些问题呢?

  • 多个线程争抢锁的时候,只有一个线程能抢到锁
  • 没有抢到锁的线程需要排成一个队列
  • 抢到锁的线程执行完释放锁之后,需要通知其他线程来抢锁

对于以上三个问题,整体思路是在同一个目录下每个线程创建一个临时序列节点,这样每个线程创建的节点名称就会是xxxx0000000001xxxx0000000002xxxx0000000003这种形式。我们假定节点序号最小的那个节点就是锁,也就是说哪个线程创建的节点序号最小,就认为哪个线程抢到了锁,其余的线程暂时只能等着。等待抢到锁的线程执行完成后删除自己创建的节点。再通过watch机制来通知其他线程争抢锁。

这样相当于实现了锁的两个部分:队列同步器。按序号排列的节点相当于处在同一个队列中。线程通过创建、删除节点来表示获取、释放锁,相当于一个同步器。

除此之外,还有一点需要被关注。抢到锁的线程执行完成,释放锁(删除Znode)之后,是否需要通知所有的节点。如果是的话,那么每次释放锁之后,所有的线程都来竞争锁,但是只会有一个线程抢到锁,其余的线程又得被阻塞,这样势必会导致大量的无意义的上下文切换。这类问题有一个专业术语叫惊群效应

为了避免这个问题,每次锁被释放后,应该只通知下一个获取锁的线程。也就是自己被删除后,下一个创建了最小节点的线程。这就要求每个节点在注册watch事件时,不能watch整个锁目录,只能watch自己的前一个节点。

整个锁的示意图如下
zookeeper分布式锁
每个Znode只watch自己的前一个节点,这样锁释放后只需要通知一个节点即可。

源代码

获取Zookeeper对象

package com.sicimike.zk.lock;

import com.google.common.base.Joiner;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * @author sicimike
 * @create 2020-05-03 21:15
 */
public class ZookeeperUtil {

	// /zkLock表示指定锁的根目录,后续获取"/"都是这个目录
    private static final String[] ZK_SERVER = {
            "192.168.1.101:2181", "192.168.1.102:2181",
            "192.168.1.103:2181", "192.168.1.104:2181/zkLock",
    };
    private static final int SESSION_TIMEOUT = 10000;
    private static final CountDownLatch latch = new CountDownLatch(1);


    public static ZooKeeper newInstance() throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper(Joiner.on(",").join(ZK_SERVER), SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                switch (event.getState()) {
                    case SyncConnected:
                        latch.countDown();
                        break;
                    case Expired:
                        break;
                }
            }
        });
        latch.await();
        return zooKeeper;
    }
}

锁的实现类

package com.sicimike.zk.lock;

import org.apache.zookeeper.ZooKeeper;

/**
 * @author sicimike
 * @create 2020-05-07 21:01
 */
public class ZookeeperLock {


    private String threadName;
    private static ZooKeeper zookeeper;
    LockWatcher lockWatcher;

    static {
        try {
            zookeeper = ZookeeperUtil.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void lock() throws InterruptedException {
        lockWatcher = new LockWatcher();
        lockWatcher.setZooKeeper(zookeeper);
        lockWatcher.setZookeeperLock(this);
        lockWatcher.setThreadName(threadName);
        lockWatcher.lock();
    }

    public void unlock() {
        try {
            lockWatcher.unlock();
            System.out.println(threadName + " unlock");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ZooKeeper getZookeeper() {
        return zookeeper;
    }

    public void setZookeeper(ZooKeeper zookeeper) {
        this.zookeeper = zookeeper;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }
}

事件处理

package com.sicimike.zk.lock;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @author sicimike
 * @create 2020-05-07 21:06
 */
public class LockWatcher implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback, AsyncCallback.StatCallback {

    private CountDownLatch latch = new CountDownLatch(1);

    private String threadName;
    private String pathName;
    private ZooKeeper zooKeeper;
    private ZookeeperLock zookeeperLock;

    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case NodeDeleted:
                zooKeeper.getChildren("/", false, this, "");
                break;
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, String name) {
        if (name != null) {
            System.out.println(threadName + " create Znode " + name);
            pathName = name;
            zooKeeper.getChildren("/", false, this, "");
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
        Collections.sort(children);
        int index = children.indexOf(pathName.substring(1));
        if (index == 0) {
            // 表示自己是最小的节点,相当于自己抢到了锁
            System.out.println(threadName + " get lock ");
            latch.countDown();
        } else {
            // 如果不是最小的节点,则判断比自己小的节点是否存在
            zooKeeper.exists(path + children.get(index - 1), this, this, "");
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {

    }

    public void lock() throws InterruptedException {
        // 创建临时序列节点
        zooKeeper.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "");
        latch.await();
    }

    public void unlock() throws KeeperException, InterruptedException {
        zooKeeper.delete(pathName, -1);
    }

    public ZooKeeper getZooKeeper() {
        return zooKeeper;
    }

    public void setZooKeeper(ZooKeeper zooKeeper) {
        this.zooKeeper = zooKeeper;
    }

    public ZookeeperLock getZookeeperLock() {
        return zookeeperLock;
    }

    public void setZookeeperLock(ZookeeperLock zookeeperLock) {
        this.zookeeperLock = zookeeperLock;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }

    public String getPathName() {
        return pathName;
    }

    public void setPathName(String pathName) {
        this.pathName = pathName;
    }
}

测试代码

package com.sicimike.zk;

import com.sicimike.zk.lock.ZookeeperLock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author sicimike
 * @create 2020-05-03 12:05
 */
public class ZookeeperDemo {

    private static final int THREAD_COUNT = 6;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                // 每个线程一把锁
                ZookeeperLock lock = new ZookeeperLock();
                lock.setThreadName(Thread.currentThread().getName());
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " working ...");
                    lock.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        while (true) {}
    }
}

执行结果

pool-1-thread-3 create Znode /lock0000000060
pool-1-thread-4 create Znode /lock0000000061
pool-1-thread-5 create Znode /lock0000000062
pool-1-thread-1 create Znode /lock0000000063
pool-1-thread-6 create Znode /lock0000000064
pool-1-thread-2 create Znode /lock0000000065
pool-1-thread-3 get lock 
pool-1-thread-3 working ...
pool-1-thread-3 unlock
pool-1-thread-4 get lock 
pool-1-thread-4 working ...
pool-1-thread-4 unlock
pool-1-thread-5 get lock 
pool-1-thread-5 working ...
pool-1-thread-5 unlock
pool-1-thread-1 get lock 
pool-1-thread-1 working ...
pool-1-thread-1 unlock
pool-1-thread-6 get lock 
pool-1-thread-6 working ...
pool-1-thread-6 unlock
pool-1-thread-2 get lock 
pool-1-thread-2 working ...
pool-1-thread-2 unlock

从执行结果可以看出,节点的创建顺序是固定的,即使每个线程执行的顺序不一样。并且获取锁的顺序正是Znode被创建的顺序。pool-1-thread-3线程在pool-1-thread-4线程之前创建Znode,所以也会先于pool-1-thread-4线程执行。

总结

本篇主要利用了zookeeper集群实现了分布式锁的简单功能。了解分布式锁的实现过程,能帮助我们更好的使用分布式锁。

参考

  • https://zh.wikipedia.org/wiki/惊群问题
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章