六、分布式锁的实现原理
实现分布式锁的方式有许多种,比如使用数据库锁、redis等等。而Zookeeper也可以用于实现分布式锁,下面通过源码来介绍Zookeeper是如何来实现分布式锁的?
6.1 惊群效应
假设用节点"/lock"来表示这个分布式锁,我们第一时间可能会想到通过判断节点"/lock"存在与否,如果不存在则创建该节点,表示获取到这个锁,否则监听该节点,等待锁的释放。如下图所示
这种方案在客户端数据较多的时候,所有客户端都监听该"/lock"节点,而当"/lock"节点移除时,zk服务需要做大量的通知处理,导致zk服务的性能骤降。这种现象就叫惊群效应
(网上很多地方翻译成羊群效应,个人感觉惊群效应更贴切一些)
6.2 实现原理
在Zookeeper的源码中已经有分布式锁的实现,在zookeeper-recipes/zookeeper-recipes-lock目录下。在节点"/lock"下的子节点都是EPHEMERAL_SEQUENTIAL临时顺序
节点,第一个子节点为持有当前分布式锁的线程,后续的子节点监听前一个子节点,这样就解决了惊群效应的问题。
其实现的示意图如下:
大致步骤可以分为以下几步:
- 判断/lock下面是否有子节点
- 如果没有,说明当前分布式锁未被持有,则创建临时顺序子节点,节点名称为
x-sessionId-序列号
。 - 如果有,说明当前分布式锁已被持有,在该目录下创建临时顺序节点,并监听它的上一个子节点
- 当第一个子节点释放锁,第二个节点监听到后,重新调用lock()方法判断当前是不是拿到了锁。如此反复
6.3 源码解读
调用lock()方法来获取分布式锁,源码如下
public synchronized boolean lock() throws KeeperException, InterruptedException {
if (isClosed()) {
return false;
}
// 确保路径存在,不存在则创建
ensurePathExists(dir);
// 尝试获取锁
return (Boolean) retryOperation(zop);
}
底层调用LockZooKeeperOperation类的execute()方法,尝试获取锁,如果拿不到,则监听上一个子节点,代码如下
public boolean execute() throws KeeperException, InterruptedException {
do {
if (id == null) {
long sessionId = zookeeper.getSessionId();
String prefix = "x-" + sessionId + "-";
// 判断该会话是否之前已经有创建子节点,如果有,则拿之前创建好的id名,否则新建子节点
findPrefixInChildren(prefix, zookeeper, dir);
idName = new ZNodeName(id);
}
List<String> names = zookeeper.getChildren(dir, false);
if (names.isEmpty()) {
LOG.warn("No children in: {} when we've just created one! Lets recreate it...", dir);
// 重建节点
id = null;
} else {
// 对这些子节点id进行排序,并拿到比当前子节点id小的列表
SortedSet<ZNodeName> sortedNames = new TreeSet<>();
for (String name : names) {
sortedNames.add(new ZNodeName(dir + "/" + name));
}
ownerId = sortedNames.first().getName();
SortedSet<ZNodeName> lessThanMe = sortedNames.headSet(idName);
// 如果比当前子节点id小的列表不为空,则监听上一个子节点
if (!lessThanMe.isEmpty()) {
ZNodeName lastChildName = lessThanMe.last();
lastChildId = lastChildName.getName();
LOG.debug("Watching less than me node: {}", lastChildId);
Stat stat = zookeeper.exists(lastChildId, new LockWatcher());
if (stat != null) {
return Boolean.FALSE;
} else {
LOG.warn("Could not find the stats for less than me: {}", lastChildName.getName());
}
} else {
// 否则判断当前子节点id是不是最小id,如果是,则返回true,表示获取到锁
if (isOwner()) {
LockListener lockListener = getLockListener();
if (lockListener != null) {
lockListener.lockAcquired();
}
return Boolean.TRUE;
}
}
}
}
while (id == null);
return Boolean.FALSE;
}
当客户端释放锁时,调用unlock()方法,删除当前对应id的子节点
public synchronized void unlock() throws RuntimeException {
if (!isClosed() && id != null) {
try {
ZooKeeperOperation zopdel = () -> {
// 删除当前对应id的子节点
zookeeper.delete(id, -1);
return Boolean.TRUE;
};
zopdel.execute();
} catch (InterruptedException e) {
// 略
} finally {
LockListener lockListener = getLockListener();
if (lockListener != null) {
lockListener.lockReleased();
}
id = null;
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。