zookeeper分布式锁实现原理及代码

本文从四个方面来解释一下我自己对锁及分布式锁的理解,并且使用zookeeper来实现一个分布式锁

一、什么是锁
二、什么是分布式锁
三、分布式锁的实现方式
四、实现分布式锁需要注意什么
五、zookeeper分布式锁实现原理
六、zookeeper分布式锁实现代码

一、什么是锁

在没有分布式系统的时候我们的系统多为单进程系统,也就是说我们一个系统就是一个进程,这个进程里会有非常多的线程来维持着我们系统的正常运行。
当多个线程同时对我们共享资源做修改的时候,如果不进行线程同步那么将导致我们的资源信息冲突。
而线程同步的本质就是对这些共享资源加锁,保证每次只能有一个线程来获得这个锁多共享资源做修改
那么什么是锁?锁就是让我们的线程进行同步操作,使我们的共享资源能够在并发操作下不产生冲突

二、什么是分布式锁

对于锁已经有了大概了解,那么什么是分布式锁?
从字面上就可以看出来,这是一种在分布式系统中存在的一种锁。为什么出现这种锁呢?
刚刚也提到了,当存在多个线程可以同时操作某个共享资源时,就需要保证我们的线程同步,使其在修改这种共享资源时能够达到我们所预期的结果。但是到了分布式系统中,系统可能会有多份并且部署在不同的机器上,这些线程会在不同的进程中存在,这时共享资源已经不是在线程之间共享了,而是属于进程之间共享的资源,所以我们需要保证进程之间的同步,这就引出来了分布式锁的概念

三、分布式锁的实现方式

1、基于数据库乐观锁来实现
2、基于redis来实现分布式锁
3、基于zookeeper来实现分布式锁

四、实现分布式锁需要注意什么

1、排他性 保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器-上的一个线程执行。
2、这把锁要是一把可重入锁(避免死锁)
3、锁要可阻塞。保证锁超时后自动释放
4、有高可用的获取锁和释放锁功能
5、加锁和释放锁的性能要好。

五、zookeeper分布式锁实现原理

这是这篇文章的重点,怎么使用zookeeper来实现分布式锁

zookeeper(以下简称zk)来实现分布式锁主要通过zk的临时有序节点来实现的
先简单回顾以下zk的节点概念:
zk的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。如下图在这里插入图片描述

zk节点类型

1.持久节点 (PERSISTENT) 默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

2.顺序持久节点(PERSISTENT_SEQUENTIAL)
包涵持久节点特性,只是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

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

4、临时顺序节点(EPHEMERAL_SEQUENTIAL)
包涵临时节点特性:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

zk分布式锁实现原理

之前透露了一点,zk是通过临时有序节点来实现分布式锁的,那么来看一下具体步骤:

获取锁

首先,在zk当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
client1获取锁流程
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。所以获取锁失败。
获取锁失败之后Client2会向排序仅比它靠前的节点Lock1注册Watcher事件,用于监听Lock1节点是否存在。如果存在说明client1没有释放锁,继续等待client1去释放锁
在这里插入图片描述
如果又来一个客户端client3来获取锁该怎么办呢?
还是在在ParentLock下再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的,所以client3也获取锁失败。
获取锁失败之后Client3会向排序仅比它靠前的节点Lock2注册Watcher事件,用于监听Lock2节点是否存在。如果存在说明client2没有释放锁或者client2根本就没有取得锁,这时client3也会进入等待状态,等着client2去获得锁并释放锁
在这里插入图片描述
大家有没有发现,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像我们的AQS(AbstractQueuedSynchronizer)。
现在我那知道了获得锁的原理了,那下面我们看看怎么去释放锁。

释放锁

怎么去释放锁呢?很简单啊,只需要删除对应的节点就可以释放锁了。
怎么删除对应的节点呢?有两种情况:
1、当前客户端主动删除对应节点
2、客户端与zk断开的时候zk会自己删除这个客户端对应的节点。
client1任务完成或者发生宕机,这个时候client1对应的lock1节点会被删除
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小那么Client2获得了锁。
client3继续等待
在这里插入图片描述
这个时候有人会问:
client1获得锁,client2监听lock1节点,client3监听lock2节点,那么client2由于某些原因与zk断开连接,这个时候怎么办呢?
这里注意了,当前客户端如果没有获取到锁那么会对当前客户端对应节点的上个节点进行监听,如果client2断开连接,zk删除了lock2节点,client3由于监听了lock2节点所以会收到通知,然后Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的,所以client3还是获取锁失败。
获取锁失败之后Client3会向排序仅比它靠前的节点Lock1注册Watcher事件,用于监听Lock1节点是否存在。如果存在说明client1没有释放锁或者client1根本就没有取得锁,这时client3也会进入等待状态,等着client1去获得锁并释放锁

在这里补充一下,zk分布式锁分为两种,一种共享锁,一种独占锁。
刚刚上面的原理是共享锁的原理,至于独占锁原理是什么,这里没有详细说明,可以简单提一下,独占锁是使用zk的临时节点来实现,并没有使用临时有序节点。大致步骤是这样的:
client1去ParentLock下面创建临时节点lock,如果创建成功代表获取锁成功
client2去ParentLock下面创建临时节点lock,因为client1已经创建了lock节点所以client2创建失败,这时client2会对lock节点进行监听,等待client1释放锁

六、zookeeper分布式锁实现代码

为了更好的理解zookeepr分布式锁,本文采用zk原生的API来实现一个简单的分布式锁,这里只是一个简单的实现,
在真实情况下肯定不会用这种方式来实现。一般使用curator框架来实现zk分布式锁。

创建zk客户端

public class ZookeeperClient {
     
     

   //zk地址
   private final static String ZKIP = "192.168.2.110:2181";

   //会话超时时间
   private static int SESSIONTIMEOUT = 5000;

   public static ZooKeeper getZkClient() throws IOException, InterruptedException {
     
     

       final CountDownLatch countDownLatch = new CountDownLatch(1);

       // 去连接zookeeper server, 创建会话的时候,是异步进行的
       // 所有要给一个监听器,说告诉我们什么时候是真正的完成跟zookeeper server 连接
       ZooKeeper zooKeeper = new ZooKeeper(ZKIP, SESSIONTIMEOUT, new Watcher() {
     
     
           @Override
           public void process(WatchedEvent watchedEvent) {
     
     
               //这里判断连接状态是不是连接成功的
               if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
     
     
                   countDownLatch.countDown();
               }
           }
       });

       countDownLatch.await();
       return zooKeeper;
   }
}

创建Watcher监听,用来监听节点的变化

public class LockWacher implements Watcher {
     
     

   private CountDownLatch countDownLatch;

   public LockWacher(CountDownLatch countDownLatch) {
     
     
       this.countDownLatch = countDownLatch;
   }

   @Override
   public void process(WatchedEvent watchedEvent) {
     
     
       //监听节点是否被删除
       if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
     
     
           countDownLatch.countDown();
       }
   }

}

创建锁逻辑

public class DistributeLock {
     
     

   //根节点
   private static final String ROOT_LOCK = "/ParentLock";
   //zk实例
   private ZooKeeper zooKeeper;
   //会话超时时间
   private int sessionTimeOut = 5000;
   //记录锁节点id
   private String lockId;

   CountDownLatch countDownLatch = new CountDownLatch(1);

   //节点初始化数据
   private final static byte[] date = {
     
     1};

   public DistributeLock() throws IOException, InterruptedException {
     
     
       this.zooKeeper = ZookeeperClient.getZkClient();
   }
   
   //获取锁
   public boolean lock() throws KeeperException, InterruptedException {
     
     
       //创建临时有序节点获取节点id
       lockId = zooKeeper.create(ROOT_LOCK + "/", date,
               ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
       System.out.println(Thread.currentThread().getName() +
               "===>成功创建节点" + "===>节点id为【" + lockId+"】开始竞争锁");

       //获取锁逻辑
       //1、获取根节点下所有子节点
       List<String> childNodes = zooKeeper.getChildren(ROOT_LOCK, true);
       //2、对子节点进行排序,从小到大
       SortedSet<String> sortedSet = new TreeSet<String>();
       for (String child:childNodes) {
     
     
           sortedSet.add(ROOT_LOCK + "/" + child);
           
       }
       //3、拿到最小节点
       String first = sortedSet.first();
       //4、如果当前节点是最小节点则表示获取到锁
       if(lockId.equals(first)){
     
     
           System.out.println(Thread.currentThread().getName() +
                   "===>成功获得锁" + "===>节点id为【" + lockId+"】");
       }
       //5、如果没有获取锁,则需要拿到比当前节点的上一个节点对它监听
       //取得比当前节点小的的节点集合
       SortedSet<String> lessthenLockId = sortedSet.headSet(lockId);
       if(!lessthenLockId.isEmpty()){
     
     
           //拿到比当前节点小的上一个节点
           String lastNode = lessthenLockId.last();
           //对这个节点进行监控
           zooKeeper.exists(lastNode, new LockWacher(countDownLatch));

           countDownLatch.await(sessionTimeOut, TimeUnit.MILLISECONDS);

           System.out.println(Thread.currentThread().getName() +
                   "===>成功获得锁" + "===>节点id为【" + lockId+"】");
       }

       return true;
   }

  //释放锁
   public boolean unlock() throws KeeperException, InterruptedException {
     
     
       System.out.println(Thread.currentThread().getName() +
               "===>开始释放锁" + "===>节点id为【" + lockId+"】");
       zooKeeper.delete(lockId,-1);
       System.out.println(Thread.currentThread().getName() +
               "===>释放锁成功" + "===>节点id为【" + lockId+"】");
       return true;
   }
}

模拟测试

public class TestZkClient {
     
     
   public static void main(String[] args) throws Exception {
     
     
       
      final CountDownLatch countDownLatch = new CountDownLatch(5);

       Random random = new Random();

       for (int i = 0; i < 5; i++) {
     
     
           new Thread(() -> {
     
     
               DistributeLock lock= null;
               try {
     
     
                   lock = new DistributeLock();
                   countDownLatch.countDown();
                   countDownLatch.await();
                   lock.lock();
                   Thread.sleep(random.nextInt(1000));
               } catch (InterruptedException e) {
     
     
                   e.printStackTrace();
               } catch (KeeperException e) {
     
     
                   e.printStackTrace();
               } catch (IOException e) {
     
     
                   e.printStackTrace();
               } finally {
     
     
                   if(lock!=null){
     
     
                       try {
     
     
                           lock.unlock();
                       } catch (KeeperException e) {
     
     
                           e.printStackTrace();
                       } catch (InterruptedException e) {
     
     
                           e.printStackTrace();
                       }
                   }
               }
           }).start();
       }
   }
}

执行结果

Thread-2===>成功创建节点===>节点id为【/ParentLock/0000000000】开始竞争锁
Thread-3===>成功创建节点===>节点id为【/ParentLock/0000000001】开始竞争锁
Thread-0===>成功创建节点===>节点id为【/ParentLock/0000000002】开始竞争锁
Thread-4===>成功创建节点===>节点id为【/ParentLock/0000000003】开始竞争锁
Thread-1===>成功创建节点===>节点id为【/ParentLock/0000000004】开始竞争锁
Thread-2===>成功获得锁===>节点id为【/ParentLock/0000000000】
Thread-2===>开始释放锁===>节点id为【/ParentLock/0000000000】
Thread-2===>释放锁成功===>节点id为【/ParentLock/0000000000】
Thread-3===>成功获得锁===>节点id为【/ParentLock/0000000001】
Thread-3===>开始释放锁===>节点id为【/ParentLock/0000000001】
Thread-3===>释放锁成功===>节点id为【/ParentLock/0000000001】
Thread-0===>成功获得锁===>节点id为【/ParentLock/0000000002】
Thread-0===>开始释放锁===>节点id为【/ParentLock/0000000002】
Thread-0===>释放锁成功===>节点id为【/ParentLock/0000000002】
Thread-4===>成功获得锁===>节点id为【/ParentLock/0000000003】
Thread-4===>开始释放锁===>节点id为【/ParentLock/0000000003】
Thread-4===>释放锁成功===>节点id为【/ParentLock/0000000003】
Thread-1===>成功获得锁===>节点id为【/ParentLock/0000000004】
Thread-1===>开始释放锁===>节点id为【/ParentLock/0000000004】
Thread-1===>释放锁成功===>节点id为【/ParentLock/0000000004

执行结果分析
这个模拟五个进程,每个进程代表一个线程。
开始:线程0 1 2 3 4共同竞争锁,都向ParentLock节点下创建临时有序节点对应关系为:

Thread-2 0000000000
Thread-3 0000000001
Thread-0 0000000002
Thread-4 0000000003
Thread-1 0000000004

1、结合上面讲的原理,客户端要判断自身节点是不是最小的,很显然Thread-2的节点最小,所以第一次Thread-2获得锁,Thread-3 监听0000000000,Thread-0监听0000000001,Thread-4监听0000000002,Thread-1监听 0000000004
2、Thread-2 释放锁之后,0000000000节点删除,Thread-3 得到通知,判断对应的节点0000000001是不是最小的,显然是最小的,所以Thread-3 获得锁
3、Thread-3 释放锁之后,0000000001节点删除,Thread-0 得到通知,判断对应的节点0000000002是不是最小的,显然是最小的,所以Thread-0 获得锁
在这里插入图片描述

本文中基于zookeeper实现分布式锁就这些内容,
代码中很多细节都没有考虑,主要是想通过代码来对	zookeeper分布式锁的实现原理有更深一步的了解。
文章中有很多不足之处,请大家多多指出。

end

猜你喜欢

转载自blog.csdn.net/u010994966/article/details/93395743