ZooKeeper的典型应用场景——分布式锁

在平时的实际项目开发中,可以依赖关系型数据库固有的排他性来实现不同进程间的互斥。然而,目前绝大数大型分布式系统的性能瓶颈都集中在数据库操作上,如果上层业务再给数据库添加一些额外的锁,例如行锁等,会让数据库更加不堪重负。

排他锁

定义锁

在ZooKeeper中,通过数据节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁。

获取锁

在需要获取锁的情况下,尤其是在高并发情况下,所有的客户端都会调用create()接口,创建/exclusive_lock/lock**临时**节点,而ZooKeeper会保证在所有的客户端中,最终只会有一个客户端能够创建成功,即获取到该排他锁。
同时,没有获取到锁的客户端需要到节点上注册一个子节点变更的Watcher监听,一旦锁节点被释放,就可以再次尝试获取锁。

释放锁

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

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

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

共享锁

定义锁

和排他锁一样,通过ZooKeeper上的数据节点来表示一个锁,是一个“/shared_lock/[host-name]-请求类型-序号”格式的临时顺序节点。

获取锁

根据共享锁的定义,事务对同一对象有读读共享,读写互斥,写写互斥的原则。通过ZooKeeper的节点来确定分布式读写顺序:

  • 创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的Watcher监听
  • 确定自己的节点序号在所有子节点中的顺序
  • 读/写请求的处理:
    • 对于读请求:
      如果没有比自己序号小的子节点,或是所有比自己序号小的节点都是读请求,那么表明自己已成功获取到共享锁,同时开始执行读逻辑;
      如果比自己序号小的子节点中有写请求,那么需要等待。
    • 对于写请求:
      如果自己不是序号最小的子节点,那么需要等待
  • 接收到Watcher通知后,重复开始步骤1

释放锁

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

羊群效应——改进的分布式共享锁

羊群效应:
分布式锁中的羊群效应,简单地来说,就是在整个分布式锁的竞争过程中,大量的“Watcher通知”和“子节点列表的获取”两个操作重复运行,并且大多数节点的运行结果都是判断出自己并不是当前序号最小的节点,从而继续等待下一次通知,而不是执行业务逻辑。
结果是,造成ZooKeeper服务器巨大的性能影响和网络冲击,更甚的是如果同一时间多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其余客户端发送大量的事件通知。

主要改动:每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可。具体实现如下:

  • 客户端调用create()方法创建一个格式为“/shared_lock/[host-name]-请求类型-序号”的临时顺序节点
  • 客户端调用getChildren()接口来获取所有已经创建的子节点列表,注意,这里不用注册Watcher
  • 如果无法获取到共享锁,那么就调用exist()来对比自己小的那个节点注册Watcher:
    • 对于读请求:向比自己序号小的最后一个写请求节点注册Watcher监听
    • 对于写请求: 向比自己序号小的最后一个节点注册Watcher监听
  • 等待Watcher通知,重复步骤2

    在集群规模不大、网络资源丰富的情况下,使用第一种分布式锁的实现方式是简单实用的选择;反之,可以使用改进改的分布式锁能够精细化地控制分布式锁机制。

猜你喜欢

转载自blog.csdn.net/pierce_liu/article/details/80557129