分布式锁
- 分布式锁是控制分布式系统之间同步访问资源的一种方式,如果不同系统是同一个系统的不同主机之间共享一个或一组资源,那么访问这些资源的时候,往往需要通过一些呼哧手段来防止彼此之间的干扰保证统一性,因此需要分布式锁。
排他锁
- 简称X锁,或写锁,独占锁,是一种基本的锁类型,如果事务T1对数据对象Q1加上排他锁,那么整个加锁期间,只允许事务T1对Q1进行读,写操作,其他任何事务不能对Q1操作,直到T1释放
定义锁
- Java开发中有两种方式,synchronized机制和JDK5中的ReentrantLock,然而,在Zookeeper中,没有类似于这样的API可以使用,Zookeeper通过数据节点表示一个锁,例如/exclusive_lock/lock节点被定义位一个锁
获取锁
- 获取排他锁时,所有客户端调用create接口在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock,Zookeeper保证只有一个客户端创建成功,我们认为这个创建成功的客户端就获取到锁,其他客户端需要在/exclusive_lock/lock节点上注册一个watcher监听,以便实时监听Lock节点变化
释放锁
- /exclusive_lock/lock是一个临时节点,以下两种情况可认为释放
- 当前获取锁的客户端机器宕机,ZooKeeper上的临时节点会被自动移除
- 正常执行业务逻辑后,客户端自行删除临时节点
- 无论上面两种哪一种,Zookeeper删除临时节点后/exclusive_lock/lock上注册Watcher监听的客户端都会收到Watcher通知,再次重新发起分布式锁的获取,重复以上流程,如下图所示排他锁流程
共享锁
- 共享锁简称S锁,又称为读锁,同样是一种基本锁,如果T1对Q1加上共享锁,那么当前事务只能多Q1进行读,其他事务也只能对这个数据加共享锁,直到所有共享锁都释放。下面借助ZooKeeper来实现共享锁
定义锁
- 与排他锁一样,同样通过ZooKeeper节点表示锁,类似/shared_lock/[Hostname]-请求类型-序号,的临时顺序节点,例如/shared_lock/129.168.1.1-R-0000001那么这个代表一个共享锁
获取锁
- 客户端到/shared_lock这个节点下创建一个临时顺序节点,如果当前是读请求,那么就叉棍就如/shared_lock/129.168.1.1-R-0000001的节点,如果是写/shared_lock/129.168.1.1-W-0000001
判断读写顺序
- 根据共享锁定义,不同事务可以同一时间多一个数据读,而更新只能单独进行,我们通过Zoo keep如下方式来进行:
- 创建节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变化的Watcher监听
- 确定字节节点序号在所有子节点中顺序
- 对于读请求:如果没有比字节序号小的子节点,或者是所有比字节序号小的子节点都是读,表明字节已经成功获取共享锁,同时开始读
- 对于写请求:如果子不是序号最小的子节点,进入等待
- 接收Watcher后,重复步骤1
释放锁
- 与排他锁一致,删除释放
- 用如下图解释
羊群效应
- 在规模不大,10台机器作业,上面方案能有很高的效率,如果规模变大,会存如下的这种问题,在判断读写顺序的时候,会给子节点列表添加watcher监听,这样任何一个子节点修改都会有watcher通知信息,如下图:
- 192.168.0.1 这条机器首先进行读,然后删除节点
- 余下四台机器都能收到子节点移除通知,然后从/shared_lock节点上获取一份新子节点列表
- 每个机器判断自己读写顺序,其中192。168.0.2这台机器检测自己是最小序号,开始写操作,余下继续等待
- 继续2 步骤
- 上面操作是共享锁实现主要步骤,当任意一个子节点变化后,Watcher通知会给所有其他集群通知,然而现实是这个通知其实只对192.168.0.2这个机器有用,对其他节点来说无任何作用。
- 后果: 分布式竞争中,满天飞的无效watcher通知,和无效的子节点获取,然后进行等待,无意义的操作会消耗非常多的系统性能,对Zookeeper服务器造成巨大性能影响和网络冲击,更严重的是如果同一时间有多个节点对应的客户端完成事务或是事务中断引起节点变化,Zookeeper需要在短时间内向其余客户端发送大量事件通知—这就是羊群效应。
改进后的分布式锁实现
- 主要改动:每个竞争者,只需关注/shared_lock节点下序号比自己小的节点是否存在即可:
- 客户端create创建"类似/shared_lock/[Hostname]-请求类型-序号" 的临时节点
- 客户端调用getChildren接口获取所有已经创建子节点列表(不注册watcher)
- 如果无法获取共享锁,调用exist对比比自己小的节点注册watcher(id比自己小,可用时间戳)
- 读请求:向比自己序号小的最后一个写请求节点注册watcher监听
- 写请求:向比自己序号小的最后一个节点注册watcher监听
- 等待watcher通知,继续2 步骤
- 如下图:
注意
- 并不是一定每次都用改进后的,因为改进版本流程复杂,如同我们在多线程并发编程中会尽量缩小锁的范围,对分布式锁的实现改进也是这样的思路,对开发时候来说,应该具体情况具体分析:集群规模不大,网络资源丰富,第一张分布式锁简单,如果集群达到一定程度,希望能精细化处理分布式锁,可以用第二种分布式锁实现。
上一篇Zookeeper实践与应用-- Nginx负载均衡差异
下一篇Zookeepe实践与应用–分布队列