Zookeeper应用场景-分布式锁

分布式锁定义

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

在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是一种非常简便且被广泛使用的分布式锁实现方式。然而有一个不争的事实是,目前绝大多数大型分布式系统的性能瓶颈都集中在数据库操作上,因此,如果上层业务再给数据库添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,那么就会让数据库更加不堪重负。

下面我们来看看使用zookeeper如何实现分布式锁,这里主要讲解排他锁和共享锁两类分布式锁。

排他锁

排他锁基本概念

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

从上面讲解的排他锁的基本概念中,我们可以看到,排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。

下面我们就来看看如何借助zookeeper实现排他锁

定义锁

在通常的Java 开发编程中,有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然而,在zookeeper中,没有类似于这样的API可以直接使用,而是通过zookeeper上的数据节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁,如图

在这里插入图片描述

获取锁

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

释放锁

在定义锁部分,我们已经提到,/exclusive_lock/lock是一个临时节点,因此在以下两种情况下,都有可能释放锁。
1,当前获取锁的客户端发生宕机,那么zookeeper上的这个临时节点就会被移除。
2,正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除。
无论在什么情况下移除了lock 节点,zookeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复获取锁的过程。整个排他锁的获取和释放流程如下图

在这里插入图片描述

共享锁

共享锁概念

共享锁(Shared Locks,简称S锁),又称为读锁,同样是一种基本的锁类型

如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放

共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对一个事务可见,而加上共享锁后,数据对所有事务都可见

下面我们就来看看如何借助zookeeper来实现共享锁

定义锁

和排他锁一样,同样是通过zookeeper上的数据节点来表示一个锁,是一个类似于"/shared_lock/[Hostname]-请求类型-序号"的临时顺序节点,例如/shared_lock/host1-R- 0000000001,那么,这个节点就代表了一个共享锁,如下图所示
在这里插入图片描述

获取锁

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

判断读写顺序
通过zookeeper来确定分布式读写顺序,大致分为以下四步

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

释放锁

释放锁的过程与排他锁一样。

羊群效应

上面讲解的这个共享锁实现,大体上能够满足一般的分布式集群竞争锁的需求,并且性能都还可以。这里说的一般场景是指集群规模不是特别大,一般在10台机器以内。但是如果机器规模扩大之后,会有什么问题呢?我们着重来看上面 判断读写循序 过程的步骤3,结合下图,看看实际运行中的情况
在这里插入图片描述
针对如上图所示的情况进行分析

  1. host1首先进行读操作,完成后将节点/shared_lock/host1-R-00000001删除
  2. 余下4台机器均收到这个节点移除的通知,然后重新从/shared_lock节点上获取一份新的子节点列表
  3. 每台机器判断自己的读写顺序,其中host2检测到自己序号最小,于是进行写操作,余下的机器则继续等待。
  4. 继续。。。

可以看到,host1客户端在移除自己的共享锁后,zookeeper发送了子节点变更Watcher通知后所有机器,然后除了给host2产生影响外,对其他机器没有任何作用,大量的Watcher通知和子节点列表获取两个操作会重复执行,这样不仅会对zookeeper服务器造成巨大的性能影响和网络开销,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或者事务中断引起节点消失,zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知,这就是所谓的羊群效应

上面这个zookeeper分布式共享锁实现中出现羊群效应的根源在于,没有找准客户端真正的关注点。我们再来回顾一下上面的分布式锁竞争过程,它的核心逻辑在于:判断自己是否是所有子节点中序号最小的。于是,很容易可以联想到,每个节点对应的客户端只需要关注比自己序号小的那个相关节点的变更情况就可以了,而不需要关注全局的子列表变更情况。

可以通过如下改动来避免羊群效应

首先,我们需要肯定的一点是,上面提到的共享锁实现,从整体思路上来说完全正确。这里主要的改动在于:每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可,具体实现如下

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

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值