ZooKeeper 分布式锁 Curator 源码 (可重入锁)

前言 一般工作中常用的分布式锁,就是基于 Redis 和 ZooKeeper,前面已经介绍完了 Redisson 锁相关的源码,下面一起看看基于 ZooKeeper 的锁。也就是 Curator 这个框架。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

Curator 的锁也分为很多种,本文分析共享可重入锁。

考虑到如果文章篇幅较长,不太适合阅读,所以对文章做了适当的拆分。
准备 环境配置
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

本机三个节点
版本:3.7.0
系统:macOS
安装方式:brew install zookeeper
Curator Maven 依赖版本:5.1.0
org.apache.curator curator-recipes 5.1.0

加锁示例
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

详细信息可参考官方文档。
加锁前 ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

在加锁之前,ZooKeeper 仅有一个节点 /zookeeper
加锁中 在 /locks/lock_01 路径上加锁。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

加锁之后:
  1. 创建了一个 /locks/lock_01 的持久节点,节点下有一个子节点 _c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
  2. 节点 /locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000 是临时节点
  3. 节点 /locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000 的数据是机器 IP 地址
锁流程 加锁源码
PS:下面代码截图中的代码风格就是 Curator 源码的代码风格。
入口 InterProcessMutex#internalLock
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

开始先从 threadData 中获取当前线程,这里肯定是没有的,所以进入 attemptLock 方法。
本方法中还包含了锁重入的逻辑,后面也会介绍。
加锁 LockInternals#attemptLock
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

核心部分就是这两行:
  1. createsTheLock 创建临时顺序节点
  2. internalLockLoop 判断是否创建成功
创建临时顺序节点 StandardLockInternalsDriver#createsTheLock
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

可以看出节点的 mode 是 CreateMode.EPHEMERAL_SEQUENTIAL,表示这是一个临时顺序节点!
进入 CreateBuilderImpl#forPath(java.lang.String, byte[])
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

client.getDefaultData() 就是本机 IP 地址。
这个 adjustPath 方法看名字就是在调整路径之类的。会生成一个 UUID 拼接到 /locks/lock_01 中,变成 /locks/lock_01/_c_UUID-lock-
因为创建的是临时顺序节点,所以会自动在后面添加顺序,最终变为 /locks/lock_01/_c_UUID-lock-0000000000
具体创建节点是在 CreateBuilderImpl#pathInForeground 中。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

  1. 创建临时节点,如果路径存在,会创建成功,如果路径不存在会创建失败;
  2. 创建失败后,先创建路径,再创建节点。
小结
到这里主要介绍了基于 ZooKeeper 的分布式锁框架 Curator 的使用,以及加锁流程,源码分析。
【ZooKeeper 分布式锁 Curator 源码 (可重入锁)】下面对内容做下总结:
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

重点需要关注的是:
  1. 基于 ZooKeeper 的分布式锁,是使用的临时顺序节点,父节点是持久节点;
  2. 创建临时节点时,父节点不存在,会先创建父节点(路径);
  3. 锁的组成结构为:对 /locks/lock_01 加锁,实际锁住的是 /locks/lock_01/_c_UUID-lock-序号,举例为 /locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
锁重入及并发加锁 锁重入
在上一小节中,可以看到加锁的过程,再回头看 internalLock 这个方法。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

加锁成功之后,将当前线程放到 threadData 中,threadData 是 ConcurrentMap 类型的,不用担心并发问题。
假如锁重入了,直接就会在上一部分 lockData != null 被拦下,然后执行 lockData.lockCount.incrementAndGet();
对 lockCount 自增,代表了锁重入。
这里发现了吧!Curator 的锁重入是在 Java 代码中实现的。
锁释放
当锁需要释放的时候,只需要调用 lock.release() 进行释放即可,具体是如何释放的呢?
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

主要分为两部分:
  1. 递减 threadData 中当前线程的加锁次数;
  2. 加锁次数大于 0,说明还剩余重入次数,直接返回;
  3. 加锁次数等于 0,则 releaseLock 释放锁,并删除 threadData 中当前线程 key。
releaseLock 方法中就没有多少复杂逻辑了,就是移除监听器,删除临时顺序节点。 也就是 /locks/lock_01/_c_e855d232-c636-4241-bf8e-f047939a5833-lock-0000000001
并发加锁
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

先来看结果,在多线程对 /locks/lock_01 加锁时,是在后面又创建了新的临时节点。
这块在加锁方法 CreateBuilderImpl#pathInForeground 中已经介绍过
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

这里判断 /locks/lock_01 路径已经存在,会直接创建新的临时顺序节点。
真正判断锁是否获取成功,其实是在 LockInternals#attemptLock 方法中的 internalLockLoop 方法中。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

锁等待 加锁结果及监听
internalLockLoop 方法的主要作用是判断加锁结果,以及获取锁失败时,对其他节点的监听。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

  1. 获取父节点 /locks/lock_01 下的所有子节点,按照从小到大排序,判断自己是不是获取到锁,没有获取到就监听自己前一个节点;
  2. 支持设置超时时间,超时直接返回失败;
  3. 不支持设置超时时间或者还没有超时,则直接 wait 等待。
是否获取锁的代码在 StandardLockInternalsDriver#getsTheLock
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

这块就是判断是否为最小节点,因为在 getSortedChildren 中已经对所有节点排序,所以方法中的 List children 是有序的。
maxLeases 是在 InterProcessMutex 初始化的时候,指定的值为 1。
最终这里的结果是,判断自己是不是最小,不是最小,就将 pathToWatch 设置为前一个节点。
只监听自己的前一个节点,可以避免羊群效应!
为什么要进行等待呢?
因为是为了防止无效自旋,因为这里有监听机制,会监听上一个节点是否释放。
ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

ZooKeeper 分布式锁 Curator 源码 (可重入锁)
文章图片

这块是 ZooKeeper 的 Watcher 监听机制,在节点释放的时候,会进行回调,然后使用 Java 的 notifyAll 方法通知所有的 wait 线程。然后这里的 while trye 会继续执行,重新检查是否获得锁等。
小结
本文主要介绍了基于 ZooKeeper 的分布式锁框架 Curator 在并发场景下的锁竞争问题。
重点需要了解的是:
  1. 为了避免羊群效应,临时顺序节点,加锁失败后监听的是前一个节点;
  2. 为了避免无效自旋,这里使用了 Java 的 wait/notifyAll 机制;
  3. 可以看出,默认加锁就是公平锁。

    推荐阅读