一、设计
使用临时顺序znode来表示获取锁的请求,创建最小后缀数字znode的用户成功拿到锁。
01 设计.png
二、避免羊群效应(herd effect)
把锁请求者按照后缀数字进行排队,后缀数字小的锁请求者先获取锁。如果所有的锁请求者都watch锁持有者,当代表锁持有者的znode被删除后,所有的锁请求者都会都会通知到(惊着了),但是只有一个锁请求者能拿到锁。这就是羊群效应。
02锁持有者.png
三、代码结构
03代码结构.png-
org.apache.zookeeper.recipes.lock.ZNodeName:临时节点名称,能够根据sequence进行排序。实现了Comparable接口。名称中以'-'分割,并且在构造函数中提取sequence值。compareTo方法中,先根据sequence比较,如果相等,再根据前缀prefix比较:
04 compareTo.png -
org.apache.zookeeper.recipes.lock.ZooKeeperOperation:锁实现接口。并且能够实现重试操作。主要实现类为org.apache.zookeeper.recipes.lock.WriteLock中的私有内部类:org.apache.zookeeper.recipes.lock.WriteLock.LockZooKeeperOperation,为实际的获取锁操作类。
-
org.apache.zookeeper.recipes.lock.LockListener:锁监控接口,定义了获取锁和释放锁的回调方法。
-
org.apache.zookeeper.recipes.lock.ProtocolSupport:主要提供retryOperation等同步操作。
-
org.apache.zookeeper.recipes.lock.WriteLock:互斥写锁的主要实现,主要是选举一个leader节点。通过调用lock()方法来尝试获取锁。可以注册一个监听器LockListener在获取锁或者是释放锁的时候调用。也可以通过调用 isOwner()来询问是否拥有锁:
05 lock.png
四、调试跟踪
为了避免超时,更改org.apache.zookeeper.test.ClientBase类中的超时时间设置:
06 clientBase.png
进入org.apache.zookeeper.recipes.lock.WriteLock中的lock()方法:
07 断点lock方法.png
进入org.apache.zookeeper.recipes.lock.ProtocolSupport中的ensureExists方法,判断目录是否存在:
08 断点ensureExists方法.png
如果不存在,则创建,且创建模式为PERSISTENT
09 PERSISTENT.png 10 断点ensureExists方法.png
进入retryOperation方法,通过重试机制(默认重试次数retryCount为10次)执行接口ZooKeeperOperation实现类的execute()方法,
11 断点retryOperation方法.png
具体为执行org.apache.zookeeper.recipes.lock.WriteLock.LockZooKeeperOperation类的execute()方法
努力尝试查找最小后缀数字的znode节点成功拿到锁。
12 execute方法.png
13 锁判断.png
然后再创建一个客户端,再次进行加锁:
14 执行另一个锁.png
15 另一个锁目录判断.png
16案例执行成功.png
五、相关问题
- 对于分布式锁的场景,如果创建的是临时节点,当T1请求获取锁后,执行相应业务逻辑,但是此时业务逻辑还没有执行完成,因网络原因导致session过期,临时节点就会被服务端删除。这样的话,其他节点也可以获取锁,分布式锁就被破坏了。对于此种问题,可以通过创建持久性节点来解决。
- znode是否类似于Redis中的Key的概念?
可以把znode理解成Redis的一个Key,但是znode之间有层次关系。 - 在某些场景,经常用Redis做分布式锁(setnx命令),只是redis没有将请求者进行排队, 与 zookeeper的分布式锁有和区别?
如果一个调用setnx的Redis客户端crash,它设置的key还会存在,换言之锁不会自动释放。在ZooKeeper里面,我们用临时节点表示锁,如果ZooKeeper客户端crash,它的锁会自动释放;ZooKeeper实现的锁可以在锁释放时只通知一个锁请求者,还保证锁分配的FIFO。Zookeeper的锁方案更加完备。
另外Redis(https://redis.io/commands/setnx)本身也不推荐使用setnx了。 - zookeeper分布式锁为了避免羊群效应,采用的是公平锁。但是公平锁有一个副作用:
比如节点1获得了锁,节点2客户端watch节点1,节点3客户端watch节点2,如果此时节点2的客户端心跳失败,触发watch机制,节点3的客户端要更换watch节点,也就是会watch锁持有者节点1,否则一旦节点1释放锁后,其他节点客户端就会永远感知不到。
但是非公平锁,不会存在这个问题。中间其它未持有锁的client端的session失效,并不会对其他客户端产生影响。在锁竞争并不会特别激烈的场景下,非公平锁的性能会更佳。正因为如此,jdk下JUC包里面的锁类默认都采用非公平模式。
网友评论