ZooKeeper 分布式锁 Curator 源码 (可重入锁)
前言
一般工作中常用的分布式锁,就是基于 Redis 和 ZooKeeper,前面已经介绍完了 Redisson 锁相关的源码,下面一起看看基于 ZooKeeper 的锁。也就是 Curator 这个框架。
文章图片
Curator 的锁也分为很多种,本文分析共享可重入锁。
考虑到如果文章篇幅较长,不太适合阅读,所以对文章做了适当的拆分。准备 环境配置
文章图片
本机三个节点
版本:3.7.0
系统:macOS
安装方式:brew install zookeeper
Curator Maven 依赖版本:5.1.0
org.apache.curator
curator-recipes
5.1.0
加锁示例
文章图片
详细信息可参考官方文档。
加锁前
文章图片
在加锁之前,ZooKeeper 仅有一个节点
/zookeeper
。加锁中 在
/locks/lock_01
路径上加锁。文章图片
加锁之后:
- 创建了一个
/locks/lock_01
的持久节点,节点下有一个子节点_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
- 节点
/locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
是临时节点 - 节点
/locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
的数据是机器 IP 地址
PS:下面代码截图中的代码风格就是 Curator 源码的代码风格。入口
InterProcessMutex#internalLock
文章图片
开始先从 threadData 中获取当前线程,这里肯定是没有的,所以进入 attemptLock 方法。
本方法中还包含了锁重入的逻辑,后面也会介绍。
加锁
LockInternals#attemptLock
文章图片
核心部分就是这两行:
- createsTheLock 创建临时顺序节点
- internalLockLoop 判断是否创建成功
StandardLockInternalsDriver#createsTheLock
文章图片
可以看出节点的 mode 是
CreateMode.EPHEMERAL_SEQUENTIAL
,表示这是一个临时顺序节点!进入
CreateBuilderImpl#forPath(java.lang.String, byte[])
文章图片
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 的使用,以及加锁流程,源码分析。
【ZooKeeper 分布式锁 Curator 源码 (可重入锁)】下面对内容做下总结:
文章图片
重点需要关注的是:
- 基于 ZooKeeper 的分布式锁,是使用的临时顺序节点,父节点是持久节点;
- 创建临时节点时,父节点不存在,会先创建父节点(路径);
- 锁的组成结构为:对
/locks/lock_01
加锁,实际锁住的是/locks/lock_01/_c_UUID-lock-序号
,举例为/locks/lock_01/_c_cc4fc045-5a1e-4378-b3c7-8a8d3fb9a37c-lock-0000000000
在上一小节中,可以看到加锁的过程,再回头看
internalLock
这个方法。文章图片
加锁成功之后,将当前线程放到
threadData
中,threadData 是 ConcurrentMap
类型的,不用担心并发问题。假如锁重入了,直接就会在上一部分
lockData != null
被拦下,然后执行 lockData.lockCount.incrementAndGet();
。对 lockCount 自增,代表了锁重入。
这里发现了吧!Curator 的锁重入是在 Java 代码中实现的。
锁释放
当锁需要释放的时候,只需要调用 lock.release() 进行释放即可,具体是如何释放的呢?
文章图片
主要分为两部分:
- 递减 threadData 中当前线程的加锁次数;
- 加锁次数大于 0,说明还剩余重入次数,直接返回;
- 加锁次数等于 0,则 releaseLock 释放锁,并删除 threadData 中当前线程 key。
/locks/lock_01/_c_e855d232-c636-4241-bf8e-f047939a5833-lock-0000000001
。并发加锁
文章图片
先来看结果,在多线程对
/locks/lock_01
加锁时,是在后面又创建了新的临时节点。这块在加锁方法
CreateBuilderImpl#pathInForeground
中已经介绍过文章图片
这里判断
/locks/lock_01
路径已经存在,会直接创建新的临时顺序节点。真正判断锁是否获取成功,其实是在
LockInternals#attemptLock
方法中的 internalLockLoop
方法中。文章图片
锁等待 加锁结果及监听
internalLockLoop
方法的主要作用是判断加锁结果,以及获取锁失败时,对其他节点的监听。文章图片
- 获取父节点
/locks/lock_01
下的所有子节点,按照从小到大排序,判断自己是不是获取到锁,没有获取到就监听自己前一个节点; - 支持设置超时时间,超时直接返回失败;
- 不支持设置超时时间或者还没有超时,则直接 wait 等待。
StandardLockInternalsDriver#getsTheLock
文章图片
这块就是判断是否为最小节点,因为在
getSortedChildren
中已经对所有节点排序,所以方法中的 List children
是有序的。maxLeases
是在 InterProcessMutex
初始化的时候,指定的值为 1。最终这里的结果是,判断自己是不是最小,不是最小,就将 pathToWatch 设置为前一个节点。
只监听自己的前一个节点,可以避免羊群效应!为什么要进行等待呢?
因为是为了防止无效自旋,因为这里有监听机制,会监听上一个节点是否释放。
文章图片
文章图片
这块是 ZooKeeper 的 Watcher 监听机制,在节点释放的时候,会进行回调,然后使用 Java 的 notifyAll 方法通知所有的 wait 线程。然后这里的 while trye 会继续执行,重新检查是否获得锁等。
小结
本文主要介绍了基于 ZooKeeper 的分布式锁框架 Curator 在并发场景下的锁竞争问题。
重点需要了解的是:
- 为了避免羊群效应,临时顺序节点,加锁失败后监听的是前一个节点;
- 为了避免无效自旋,这里使用了 Java 的 wait/notifyAll 机制;
- 可以看出,默认加锁就是公平锁。
推荐阅读
- 深入浅出谈一下有关分布式消息技术(Kafka)
- 边走边看——锁
- 《人性的枷锁》
- 别让习惯成为可怕的枷锁
- KubeDL HostNetwork(加速分布式训练通信效率)
- stm32|基于STM32和freeRTOS智能门锁设计方案
- 实操Redission|实操Redission 分布式服务
- 分布式|《Python3网络爬虫开发实战(第二版)》内容介绍
- Java并发编程|Java并发编程 - 深入剖析ReentrantLock之非公平锁加锁流程(第1篇)
- 偏向锁、轻量级锁、重量级锁的升级以及区别