基于Zookeeper实现多进程分布式锁

2022-11-13,,

一、zookeeper简介及基本操作

Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。当对目录节点监控状态打开时,一旦目录节点的状态发生变化,Watcher 对象的 process 方法就会被调用。

创建Zookeeper实例时即可绑定一个Watcher对象,如 ZooKeeper zk = new ZooKeeper(zookeeperQuorum, sessionTimeout, Watcher; 任何实现org.apache.zookeeper.Watcher接口的类都可作为一个Watcher对象。
zookeeperQuorum=IP+端口(xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181)多个逗号隔开
可以设置观察的操作:exists,getChildren,getData
可以触发观察的操作:create,delete,setData

二、基于zookeeper的分布式锁原理

让我们来回顾一下Zookeeper节点的概念:

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。

Znode分为四种类型:

1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:

4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

Zookeeper分布式锁的原理

Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。

Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。

Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的

释放锁

释放锁分为两种情况:

1.任务完成,客户端显示释放

当任务完成时,Client1会显示调用删除节点Lock1的指令。

2.任务执行过程中,客户端崩溃

获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。

最终,Client3成功得到了锁。

三、基于zookeeper的分布式锁代码实现

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

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class DistributeLock implements Watcher{
  private ZooKeeper zk;
  //当前锁
  private String current_lock;
  //竞争的资源
  private String lockName;
  //根节点
  private String ROOT_LOCK = "/dlock";
  //由于zookeeper监听节点状态会立即返回,所以需要使用CountDownLatch(也可使用信号量等其他机制)
  private CountDownLatch latch;

  public DistributeLock(String zkAddress, String lockName) {
    this.lockName = lockName;
    try {
      zk = new ZooKeeper(zkAddress, 30000, this);
      //获取根节点状态
      Stat stat = zk.exists(ROOT_LOCK, false);
      //如果根节点不存在,则创建根节点,根节点类型为永久节点
      if(stat == null) {
        System.out.println("根节点不存在");
        zk.create(ROOT_LOCK, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,  CreateMode.PERSISTENT);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  //获取锁
  public void lock() {
    try {
      //在根节点下创建临时顺序节点,返回值为创建的节点路径
      current_lock = zk.create(ROOT_LOCK + "/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,  CreateMode.EPHEMERAL_SEQUENTIAL);
      //获取根节点下的所有临时顺序节点,不设置监视器
      List<String> children = zk.getChildren(ROOT_LOCK, false);
      //对根节点下的所有临时顺序节点进行从小到大排序
      Collections.sort(children);
      //判断当前节点是否为最小节点,如果是则获取锁,若不是,则找到自己的前一个节点,监听其存在状态
      int curIndex = Collections.binarySearch(children, current_lock.substring(current_lock.lastIndexOf("/") + 1));
      // if(current_lock.equals(ROOT_LOCK + "/" + children.get(0))) {
      if(curIndex == 0) {
        System.out.println("获取锁成功");
        return;
      }else {
        //获取当前节点前一个节点的路径
        // String prev = children.get(Collections.binarySearch(children, current_lock) - 1);
        String prev = children.get(curIndex - 1);
        //监听当前节点的前一个节点的状态
        Stat stat = zk.exists(ROOT_LOCK + "/" + prev, true);
        //此处再次判断该节点是否存在,该步骤也可省略
        if(stat == null) {
          System.out.println("获取锁成功");
          return;
        }else {
          System.out.println("等待锁......");
          latch = new CountDownLatch(1);
          //进入等待锁状态
          latch.await();
          System.out.println("获取锁成功");
          latch = null;
        }
      }
    } catch (KeeperException e) {
      e.printStackTrace();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  //释放锁
  public void unlock() {
    try {
      //删除创建的节点
      zk.delete(current_lock, -1);
      current_lock = null;
      //关闭zookeeper连接
      zk.close();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (KeeperException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void process(WatchedEvent event) {
    if(this.latch != null) {
      latch.countDown();
    }
  }
}

启动多个进程进行测试,将以下代码复制多份,启动多个进程,观察输出结果,可以看出已成功实现多进程分布式锁

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test1{

  public static void main(String[] args) throws Exception {
    DistributeLock lock = new DistributeLock("127.0.0.1:2181", "lock");
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    lock.lock();
    System.out.println(sdf.format(new Date()) + "开始执行业务......");
    Thread.sleep(30000);
    System.out.println(sdf.format(new Date()) + "业务处理结束......");
    lock.unlock();
  }
}

基于Zookeeper实现多进程分布式锁的相关教程结束。

《基于Zookeeper实现多进程分布式锁.doc》

下载本文的Word格式文档,以方便收藏与打印。