博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ZooKeeper分布式锁
阅读量:4206 次
发布时间:2019-05-26

本文共 4814 字,大约阅读时间需要 16 分钟。

目录

  分布式锁是用来控制分布式系统各个节点同步访问共享节点的一种锁机制。ZooKeeper实现分布式锁大致思想为:每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题

注: 如果同一时间多个子节点变更,会导致ZooKeeper短时间内向客户端发送大量通知,造成较大的性能影响和网络冲击——羊群效应

排他锁

  排他锁,又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。

  排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到

实现步骤

在这里插入图片描述

【1】创建一个lock node

【2】客户端lock执行以下方式:

  • 创建(create)一个有序临时节点
  • 调用 getChildren(watch=false) 获取获取子节点列表,wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知
  • 从这个列表中,判断自己创建的节点序号是否最小,如果是则直接返回true,否则继续往下走
  • 从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法
  • 如果exist返回false,则回到步骤2
  • 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2

【3】客户端unlock调用delete删除掉节点

在这里插入图片描述
优点:

  • 避免了轮询和超时控制
  • 每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
  • 便于观测
    缺点:
  • 没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始
  • 这是一个公平锁,无法实现非公平锁

共享锁

在这里插入图片描述

  即读写锁,它允许多个读操作同时共享锁。对于写操作需要获取写锁,获取写锁的前提是不存在读锁或者写锁被其他进程获取,即写锁是排他的
实现步骤
【1】创建一个lock node
获取read锁步骤

  1. 创建(create)一个有序临时节点
  2. 调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知
  3. 从这个列表中,判断是否有序号比自己小、且路径名以“write-”开头的节点,如果没有,则直接获取读锁,否则继续以下步骤
  4. 从步骤2中获取的list中选取排在当前节点前一位的、且路径名以“write-”开头的节点,调用exist(watch=true)方法
  5. 如果exist返回false,则回到步骤2
  6. 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2

在这里插入图片描述

获取write锁步骤

  • 创建(create)一个有序临时节点
  • 调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知
  • 从这个列表中,判断自己创建的节点序号是否是最小,如果是则直接返回true,否则继续往下走
  • 从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法
  • 如果exist返回false,则回到步骤2
  • 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2
    在这里插入图片描述
    【3】客户端unlock调用delete删除掉节点
    优点:
  • 避免了轮询和超时控制
  • 每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
  • 便于观测

缺点:

  • 没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始
  • 当有非常多的read节点在等待一个write节点删除通知时,一旦write节点删除,将会触发非常多的read节点被调用,不过这种情况无法避免

基于Curator客户端实现分布式锁

  Apache Curator是一个Zookeeper的开源客户端,它提供了Zookeeper各种应用场景(Recipe,如共享锁服务、master选举、分布式计数器等)的抽象封装,接下来将利用Curator提供的类来实现分布式锁。Curator提供的跟分布式锁相关的类有5个,分别是:

Shared Reentrant Lock 可重入锁
Shared Lock 共享不可重入锁
Shared Reentrant Read Write Lock 可重入读写锁
Shared Semaphore 信号量
Multi Shared Lock 多锁

可重入锁

  Shared Reentrant Lock,全局可重入锁,所有客户端都可以请求,同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类 InterProcessMutex 来实现,它的主要方法:

// 构造方法public InterProcessMutex(CuratorFramework client, String path)public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)// 通过acquire获得锁,并提供超时机制:public void acquire() throws Exceptionpublic boolean acquire(long time, TimeUnit unit) throws Exception// 撤销锁public void makeRevocable(RevocationListener
listener)public void makeRevocable(final RevocationListener
listener, Executor executor)

不可重入锁

  Shared Lock 与 Shared Reentrant Lock 相似,但是不可重入。这个不可重入锁由类 InterProcessSemaphoreMutex 来实现,使用方法和上面的类类似。

可重入读写锁

  Shared Reentrant Read Write Lock,可重入读写锁,一个读写锁管理一对相关的锁,一个负责读操作,另外一个负责写操作;读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞);此锁是可重入的;一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁,这也意味着写锁可以降级成读锁, 比如 请求写锁 —>读锁 ---->释放写锁;从读锁升级成写锁是不行的。

  可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex,使用时首先创建一个 InterProcessReadWriteLock 实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是 InterProcessMutex。

信号量

  Shared Semaphore,一个计数的信号量类似JDK的 Semaphore,JDK中 Semaphore 维护的一组许可(permits),而Cubator中称之为租约(Lease)。有两种方式可以决定 semaphore 的最大租约数,第一种方式是由用户给定的 path 决定,第二种方式使用 SharedCountReader 类。如果不使用 SharedCountReader,没有内部代码检查进程是否假定有10个租约而进程B假定有20个租约。 所以所有的实例必须使用相同的 numberOfLeases 值。信号量主要实现类有:

InterProcessSemaphoreV2 - 信号量实现类Lease - 租约(单个信号)SharedCountReader - 计数器,用于计算最大租约数量

调用 acquire 会返回一个租约对象,客户端必须在 finally 中 close 这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉,那么这些客户端持有的租约会自动close,这样其它客户端可以继续使用这些租约。租约还可以通过下面的方式返还:

public void returnLease(Lease lease)public void returnAll(Collection
leases)

注意一次可以请求多个租约,如果 Semaphore 当前的租约不够,则请求线程会被阻塞。同时还提供了超时的重载方法。

注意:上面所讲的4种锁都是公平锁(fair)。从ZooKeeper的角度看,每个客户端都按照请求的顺序获得锁,相当公平。

多锁

  Multi Shared Lock 是一个锁的容器。当调用 acquire,所有的锁都会被 acquire,如果请求失败,所有的锁都会被 release。同样调用 release 时所有的锁都被 release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。主要涉及两个类:

InterProcessMultiLock - 对所对象实现类
InterProcessLock - 分布式锁接口类
它的构造函数需要包含的锁的集合或者一组 ZooKeeper 的 path,用法和 Shared Lock 相同

补充:Redis与Zookeeper实现分布式锁的区别

  1. 实现方式不同
      ZooKeeper实现分布式锁:通过创建一个临时节点,创建的成功节点的服务则抢占到分布式锁,可做业务逻辑。当业务逻辑完成,连接中断,节点消失,继续下一轮的锁的抢占。Redis实现分布式锁:是通过setnx命令在redis服务里面创建一个指定key,成功返回1,失败返回0,key 是唯一,会给key 设置有效期,所以创建成功则抢占到锁,实现业务逻辑,完成之后,删除该key(del), 继续下一轮锁的抢占
  2. Redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。Zookeeper分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
  3. 如果是Redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而Zookeeper的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁

转载地址:http://sbhli.baihongyu.com/

你可能感兴趣的文章
【一天一道LeetCode】#115. Distinct Subsequences
查看>>
【一天一道LeetCode】#116. Populating Next Right Pointers in Each Node
查看>>
【一天一道LeetCode】#117. Populating Next Right Pointers in Each Node II
查看>>
【一天一道LeetCode】#118. Pascal's Triangle
查看>>
【一天一道LeetCode】#119. Pascal's Triangle II
查看>>
【unix网络编程第三版】阅读笔记(三):基本套接字编程
查看>>
同步与异步的区别
查看>>
IT行业--简历模板及就业秘籍
查看>>
JNI简介及实例
查看>>
DOM4J使用教程
查看>>
JAVA实现文件树
查看>>
linux -8 Linux磁盘与文件系统的管理
查看>>
linux 9 -文件系统的压缩与打包 -dump
查看>>
PHP在变量前面加&是什么意思?
查看>>
ebay api - GetUserDisputes 函数
查看>>
ebay api GetMyMessages 函数
查看>>
php加速器 - zendopcache
查看>>
手动12 - 安装php加速器 Zend OPcache
查看>>
set theme -yii2
查看>>
yii2 - 模块(modules)的view 映射到theme里面
查看>>