ZooKeeper学习笔记十 ZooKeeper典型应用场景——分布式锁

本文学习资源来自《从Paxos到ZooKeeper分布式一致性原理与实践》 电子工业出版社

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。

在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥,但大型分布式系统的性能瓶颈往往集中在数据库操作上。

这里看看ZooKeeper如何实现分布式锁,这里主要讲解排他锁和共享锁两类分布式锁。

排他锁

Exclusive Locks,简称X锁,又称为写锁或独占锁,是一种基本的锁类型。如果事务 T 1 对数据对象 O 1 加上了排他锁,那么在整个加锁期间,只允许事务 T 1 O 1 进行读取和更新操作,其它任何事务都不能再对这个数据对象进行任何类型的操作——直到 T 1 释放了排他锁。

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

定义锁
通常的Java开发编程中,有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。在ZooKeeper中,是通过ZooKeeper上的数据节点来表示一个锁,如/exclusive_lock/lock节点就可以被定义为一个锁。

获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。ZooKeeper会保证在所有的客户端中,最络只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听 , 以便实时监听到lock节点的变更情况。

释放锁
/exclusive_lock是一个临时节点,因此在以下两种情况下,可能释放锁:

  • 当前获取锁的客户端机器宕机,那么ZooKeeper上的这个临时节点就会被移除;
  • 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除。

无论什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。

共享锁

Shared Locks,简单S锁,同样是一种基本的锁类型。如果事务 T 1 对数据对象 O 1 加上了共享锁,那么当前事务只能对 O 1 进行读取操作,其它事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。

共享锁和排他锁最根本的区别在于,加上了排他锁后,数据对象只对一个事务可见,而加上了共享锁后,数据对所有事务都可见。下面看如何借助ZooKeeper来实现共享锁。

定义锁
和排他锁一样,同样是通过ZooKeeper上的数据节点来表示一个锁,是一个类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点,如:
/shared_lock/192.168.0.1-R-00000001,那么 这个节点就代表了一个共享锁。

获取锁
在需要获取共享锁时,所有客户端都会到/shared_lock这个节点下面创建一个临时顺序节点,如果当前是读请求,那么就创建例如 /shared_lock/192.168.0.1-R-00000001的节点。如果是写请求,那么就创建例如 /shared_lock/192.168.0.1-W-00000001的节点

判断读写顺序
根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读操作,而更新操作必须在当前没有任何事务读写操作的情况下进行。基于这个原则来通过ZooKeeper的节点来确定分布式读写顺序,大概可以分为如下4个步骤:

  1. 创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册了子节点变更的Wathcher监听
  2. 确定自己的节点序号在所有子节点中的顺序
  3. 对于读请求:
    如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑。
    如果比自己序号小的子节点中有写请求,那么就需要进入等待;
    对于写请求:
    如果自己不是序号最小的子节点,那么就需要进入等待。

释放锁
释放锁的逻辑和排他锁是一致的。

羊群效应

当集群规模变大时(超过10台),整个分布式锁的竞争过程中,大量的“Watcher通知”和“子节点列表获取**两个操作在重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知。

改进后的分布式锁实现
每个节点对应的客户端只需要关注比自己序号小的那个相关节点的变更情况就可以了——而不需要关注全局的子列表变更情况。

即:每个锁竞争才,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可。

  1. 客户端调用create()方法创建一个类似于”/shared_lock/[Hostname]-请求类型-序号“的临时顺序节点
  2. 客户端调用getChildren()接口来获取所有已经创建的子节点列表,注意,这里不注册任何watcher。
  3. 如果无法获取共享锁,那么就调用exist()来对比自己小的那个节点注册Watcher。
    对于读请求,向比自己序号小的最后一个写请求节点注册Wathcer监听。
    对于写请求,向比自己序号小的最后一个节点注册Watcher监听。
  4. 等待Watcher通知,继续进入步骤2.

代码以后再记录上。

猜你喜欢

转载自blog.csdn.net/xundh/article/details/80189638
今日推荐