Zookeeper实现分布式锁

分布式锁的几种实现方式:

  目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

  在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案。

  针对分布式锁的实现,目前比较常用的有以下几种方案:

  基于数据库实现分布式锁 基于缓存(redis,memcached,tair)实现分布式锁 基于Zookeeper实现分布式锁

  现在模拟一个使用Zookeeper实现分布式锁,假设有A,B,C三台客户端去访问资源,调用zookeeper获得锁。客户端三个在zookeeper的 /locks节点下创建一个/lock节点,由于节点是唯一性的特性,只有一个人会创建成功,其余两个创建失败,会进入监听/locks节点的变化,如果/locks下子节点/lock节点发生变化,其余两个可以去拿锁,这样是否好呢? 这样子会导致惊群效应。就是一个触发使得在短时间呢会触发大量的watcher事件,但是只有一个客户端能拿到锁。所以这种方式不建议。

  有一种比较好的方法就是利用 zookeeper 的有序节点的特性,基本思路:

  1. 在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。
  2. 客户端调用createNode方法在locks下创建临时顺序节点,然后调用getChildren(“locks”)来获取locks下面的所有子节点,注意此时不用设置任何Watcher。
  3. 客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。
  4. 如果发现自己创建的节点并非locks所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
  5. 之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locks子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。

   下面看一下我的代码实现:

public class DistributedLock implements Lock, Watcher {

    private ZooKeeper zk = null;
    private String ROOT_LOCK = "/locks"; //定义根节点
    private String WAIT_LOCK; //等待前一个锁
    private String CURRENT_LOCK; //表示当前的锁
    // 作为监听前继节点阻塞
    private CountDownLatch countDownLatch; //
    //作为连接阻塞,等待成功连接事件到达才放下门闩
    private CountDownLatch connectCountDownLatch =new CountDownLatch(1);

    public DistributedLock() {

        try {
            zk = new ZooKeeper("192.168.1.101:2181",
                    10000, this);
            connectCountDownLatch.await();
            //判断根节点是否存在
            Stat stat = zk.exists(ROOT_LOCK, true);
            if (stat == null) {//如果不存在创建
                zk.create(ROOT_LOCK, "0".getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }

    }

    /**
     * 尝试获取锁
     */
    @Override
    public boolean tryLock() {

        try {
            //创建临时有序节点
            CURRENT_LOCK = zk.create(ROOT_LOCK + "/", "0".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println(Thread.currentThread().getName() + "->" +
                    CURRENT_LOCK + ",尝试竞争锁");
            List<String> childrens = zk.getChildren(ROOT_LOCK, false); //获取根节点下的所有子节点
            SortedSet<String> sortedSet = new TreeSet();//定义一个集合进行排序
            for (String children : childrens) { // 排序
                sortedSet.add(ROOT_LOCK + "/" + children);
            }
            String firstNode = sortedSet.first(); //获得当前所有子节点中最小的节点
            // 取出比我创建的节点还小的节点,没有的话为null
            SortedSet<String> lessThenMe = ((TreeSet<String>) sortedSet).headSet(CURRENT_LOCK);
            if (CURRENT_LOCK.equals(firstNode)) {//通过当前的节点和子节点中最小的节点进行比较,如果相等,表示获得锁成功
                return true;
            }
            if (!lessThenMe.isEmpty()) {
                WAIT_LOCK = lessThenMe.last();//获得比当前节点更小的最后一个节点,设置给WAIT_LOCK
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }


    @Override
    public void lock() {
        if (this.tryLock()) { //如果获得锁成功
            System.out.println(Thread.currentThread().getName() + "->" + CURRENT_LOCK + "->获得锁成功");
            return;
        }
        try {
            waitForLock(WAIT_LOCK); //没有获得锁,继续等待获得锁
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private boolean waitForLock(String prev) throws KeeperException, InterruptedException {
        //监听当前节点的上一个节点 注册事件,这里需要在默认的 watch 事件里面处理
        // 这里是我们之前提到的 watch 事件触发最后执行的 process 回调里面的 请看最下行代码
        Stat stat = zk.exists(prev, true);
        if (stat != null) {
            System.out.println(Thread.currentThread().getName() + "->等待锁" + ROOT_LOCK + "/" + prev + "释放");
            countDownLatch = new CountDownLatch(1);
            countDownLatch.await();// 进入等待,这里需要
            // watcher触发以后,还需要再次判断当前等待的节点是不是最小的
            System.out.println(Thread.currentThread().getName() + "->获得锁成功");
        }
        return true;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        System.out.println(Thread.currentThread().getName() + "->释放锁" + CURRENT_LOCK);
        try {
            // -1 表示无论如何先把这个节点删了再说
            zk.delete(CURRENT_LOCK, -1);
            CURRENT_LOCK = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public void process(WatchedEvent event) {
        if(Event.KeeperState.SyncConnected==event.getState()){
            //如果收到了服务端的响应事件,连接成功
            connectCountDownLatch.countDown();
        }
        // 事件回调 countDownLatch.countDown();
        if (this.countDownLatch != null) {
            this.countDownLatch.countDown();
        }
    }
}

  代码中实现了  Lock,Watcher 两个接口。主要用到的是lock 里面的trylock方法,尝试去获取锁。然后还有watcher里面的处理回调的方法

测试代码:

 public static void main( String[] args ) throws IOException {
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for(int i=0;i<10;i++){
            new Thread(()->{
                try {
                    countDownLatch.await();
                    DistributedLock distributedLock=new DistributedLock();
                    distributedLock.lock(); //获得锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"Thread-"+i).start();
            countDownLatch.countDown();
        }
        System.in.read();
    }

  运行结果就可以看到:

Thread-8->/locks/0000000040,尝试竞争锁
Thread-5->/locks/0000000044,尝试竞争锁
Thread-9->/locks/0000000041,尝试竞争锁
Thread-3->/locks/0000000042,尝试竞争锁
Thread-7->/locks/0000000046,尝试竞争锁
Thread-1->/locks/0000000043,尝试竞争锁
Thread-0->/locks/0000000047,尝试竞争锁
Thread-4->/locks/0000000045,尝试竞争锁
Thread-2->/locks/0000000049,尝试竞争锁
Thread-6->/locks/0000000048,尝试竞争锁
Thread-8->/locks/0000000040->获得锁成功
Thread-9->等待锁/locks//locks/0000000040释放
Thread-5->等待锁/locks//locks/0000000043释放
Thread-0->等待锁/locks//locks/0000000046释放
Thread-1->等待锁/locks//locks/0000000042释放
Thread-2->等待锁/locks//locks/0000000048释放
Thread-7->等待锁/locks//locks/0000000045释放
Thread-6->等待锁/locks//locks/0000000047释放
Thread-3->等待锁/locks//locks/0000000041释放
Thread-4->等待锁/locks//locks/0000000044释放

  这样子当我们把 Thread-8所锁的节点 0000000040 删掉   Thread-9 就会获取锁,以此类推。实现分布式锁

 

原文地址:https://www.cnblogs.com/wuzhenzhao/p/9996522.html