基于zookeeper实现分布式锁的原理

一、 排他锁

又称为写锁或者独占锁,是一种基本的锁类型。若事务T1对数据对象O1加上了排他锁,在加锁期间,仅允许事务T1对O1进行读取和更新操作,直到T1释放了锁,其他事务才能对这个数据对象进行任何类型的操作。

ZK如何使用排他锁:

1、 在Java开发中,有两种常见的方式来定义锁,分别是synchronized和ReentrantLock。ZooKeeper通过其上的一个数据节点表示一个锁,例如创建的临时节点/exclusive/lock就可以表示一个锁。

2、 在获取排他锁时,所有客户端节点都会调用create()接口,在/exclusive下创建节点/exclusive/lock。ZK保证在所有客户端中,只有一个客户端能够创建成功,就可以认为该客户端获取到锁。同时,所有没有获取到锁的客户端需要在/exclusive/lock节点注册一个Watcher监听,以便实时监听lock节点的变更情况。

3、 在下面的两种情况下,可能会释放锁。

(1)、当前获取锁的客户端机器发生宕机,ZK上的临时节点会被移除。

(2)、正常执行完业务逻辑后,客户端会主动删除创建的临时节点。

(3)、无论在哪种情况下移除了lock节点,ZK都会通知所有注册了Watcher监听的客户端。这些客户端在接收到通知后,重新发起分布式锁获取,即重复“获取锁”过程。

排他锁优化  


问题:

对于上述的场景,当大量客户端去竞争锁的时候,会发生“惊群”效应。惊群效应指的是在分布式锁竞争的过程中,大量的"Watcher通知"和“创建/exclusive/lock”两个操作重复运行,并且绝大多数运行结果都创建节点失败,从而继续等待下一次通知。若在集群规模较大的情况下,会对ZooKeeper服务器以及客户端服务器造成巨大的性能影响和网络冲击。

改进:

(1)、 客户端调用create()方法创建名为“/exclusive/lock-”节点,这里节点类型创建类型设置为EPHEMERAL_SEQUENTIAL;

(2)、 客户端调用getChildren(“/exclusive”)方法来获取所有已经创建的子节点,若发现自身的节点序号是/exclusive目录下最小的点,则获得锁;否则,监视比自己创建的节点的序列号小的最大节点,进入等待。

(3)、 这样,避免了"惊群效应",多个客户端共同等待锁,锁释放时只有一个客户端会被唤醒。

二、 共享锁


又称为读锁,若事务T1对数据对象O1加上了共享锁,当前事务只能对O1进行读取操作,其他事务只能对这个数据对象加共享锁---直到该数据对象上的所有共享锁都被释放。

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

ZK如何使用共享锁:

1、 同样使用ZK上的数据节点表示一个锁,是一个类似于“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/192.168.0.1-R-0000000001就代表了共享锁。

2、 在获取共享锁时,所有客户端都会到/shared_lock节点下创建一个临时顺序节点,若是读请求,则创建/shared_lock/192.168.0.1-R-0000000001;若是写请求,则创建/shared_lock/192.168.0.1-W-0000000001。

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

(1)创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。

(2)确定自己的节点序号在所有子节点中的顺序。

(3)对于读请求:若没有比自己序号小的子节点,或是所有比自己序号小的请求都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑;若比自己序号小的子节点中有写请求,那么需要进入等待。

对于写请求:若自己是序号最小的子节点,则执行读取逻辑;否则进入等待。

(4)接收到Watcher通知后,重复步骤(1)。

共享锁优化


问题:

对于上述的场景,当大量客户端去竞争锁的时候,会发生“惊群”效应。惊群效应指的是在分布式锁竞争的过程中,大量的"Watcher通知"和“子节点列表获取”两个操作重复运行,并且绝大多数运行结果都判断自己并非是序号最小的节点,从而继续等待下一次通知。若在集群规模较大的情况下,会对ZooKeeper服务器以及客户端服务器造成巨大的性能影响和网络冲击。

改进:

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

1、 客户端调用create()方法创建一个类似于“shared_lock/[Hostname]-请求类型-序号”的临时顺序节点。

2、 客户端调用getChildren()接口来获取所有已经创建的子节点列表,注意,这里不注册任何Watcher。

3、 若无法获取共享锁,调用exist()对比自己小的节点注册Watcher。

    读请求:向比自己序号小的最后一个写请求节点注册Watcher监听。

    写请求:向比自己序号小的最后一个节点注册Watcher监听。

4、 等待Watcher通知,继续进入步骤2。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值