zookeeper官网地址:
http://zookeeper.apache.org/
zookeeper也被称为分布式环境指挥官、分布式系统协调服务,是分布式系统中很常见的一个基础组件。本文不对zookeeper做过多赘述,只简单介绍一下zk及其相关特性,以便于理解使用zk做分布式锁的便利性。
文章目录
一、什么是zookeeper
Apache ZooKeeper是一种用于分布式系统的高性能协调服务,提供一种集中式信息存储服务。
特点:数据存储于内存中,类似于文件系统中的属性结构(文件和目录),高吞吐量和低延迟,集群可用性高

结构:zookeeper是C/S结构、即Client-Server
应用:基于zookeeper可以实现分布式统一配置中心、服务注册中心、分布式锁、分布式队列、命名服务、Master选举…等等功能。
小结:
zk 实质上就是一个高可用、高性能的小量数据存取服务,结合它的特性(临时顺序节点、watch机制、全局有序事务...),特别适合做分布式系统协调服务。
二、zookeeper核心概念
1. Session - 会话
- zk客户端与zk服务端连接之后,产生一个会话;
- 一个客户端连接一个会话,由zk分配唯一会话ID;
- 客户端以特定时间间隔发送心跳以保持会话有效;
- 超过会话时间未收到客户端心跳,则判定客户端死了;
- 会话中请求按FIFO(先进先出)顺序执行。

2. 数据模型
以 / 为根节点
节点可以包含数据以及子节点(既是文件也是文件夹)
节点的路径总是表示为规范的、绝对的、斜杠(/)分隔的路径

znode:
zookeeper中,每个节点也被被称为一个znode;特点:名称唯一,命名规范
2.1 znode - 节点类型
持久节点:不delete就一直在zk中存在
##PERSISTENT
[zk: localhost:2181(CONNECTED) 1] create /jin/y1 ""
Created /jin/y1
临时节点:客户端与zk服务端会话结束后就会被删除
##EPHEMERAL
[zk: localhost:2181(CONNECTED) 3] create -e /jin/y3 ""
Created /jin/y3
顺序节点:zookeeper给该节点名称进行顺序编号
- 十位十进制序号
- 每个父节点有一个计数器
- 计数器是带符号int(4字节),到2147483647之后将溢出(导致名称“
-2147483648”)
##PERSISTENT_SEQUENTIAL
[zk: localhost:2181(CONNECTED) 2] create -s /jin/y2 ""
Created /jin/y20000000001
临时顺序节点:会话结束就被删除的顺序节点
##EPHEMERAL_SEQUENTIAL
[zk: localhost:2181(CONNECTED) 4] create -s -e /jin/y4 ""
Created /jin/y40000000001
2.2 watch监听机制
监听如:节点的变化、节点是否存在、子节点的变化...
客户端可以在znode上设置watch, 监听znode的变化;znode发生变化时通知客户端。
注意:
1)因为watch是一次性触发器,获取时间和设置下一个watch有延迟,所以对监听没有强可靠性
2)一个watch对象只会被特定的通知触发一次
三、zookeeper实现分布式锁原理
方式一:znode节点命名唯一 + watch机制
原理:znode节点命名唯一 + watch机制

- 争抢创建同名的临时节点
- 创建成功即抢到锁,执行业务完业务代码,删掉节点
- 创建不成功就创建watcher监控锁,阻塞等待
- watch监测到锁节点被删除,取消watcher继续争抢锁,重复上述步骤
为何是用临时节点?
防止获得锁的节点出现问题,down掉之后未释放锁
缺点:惊群效应 (并发量大的时候,反复唤醒大量线程,占用系统资源,会造成很大的网络冲击,甚至可能会让zk服务挂掉)
总结:实现简单,适用于并发量小的情况。
方式二:临时顺序节点 + watch机制
原理:临时顺序节点 + watch机制

- 需要争抢锁的线程,在同一路径下依次创建顺序节点
- 创建完成后,获得所有子节点,判断自己是否是当前最小号
- 是最小号获得锁
- 不是最小号,对前一个节点注册watcher,阻塞等待
- 获得锁的节点释放锁后重复上述步骤
总结:避免了方式一的惊群效应问题,适合高并发场景。
四、Java代码实现
这里对方式二的分布式锁,做一个简单的实现;
使用的API是zkClient:
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
相关变量定义:
public class ZKDistributedLock implements Lock {
// 锁路径(临时顺序节点们的父路径)
private String lockPath;
// zk客户端
private ZkClient client;
// 这里为了方便,用ThreadLocal实现的线程安全,也可以用其他方式
// 当前节点的路径
private ThreadLocal<String> currentPath = new ThreadLocal<>();
// 前一个节点的路径
private ThreadLocal<String> beforePath = new ThreadLocal<>();
// 锁重入计数
private ThreadLocal<Integer> reentrantCount = new ThreadLocal<>();
// 下面是复写Lock的方法,这里无需关注
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
构造方法:
public ZKDistributedLock(String lockPath) {
super();
this.lockPath = lockPath;
client = new ZkClient("host:2181");
client.setZkSerializer(new MyZkSerializer());
if (!this.client.exists(lockPath)) {
try {
this.client.createPersistent(lockPath);
} catch (ZkNodeExistsException e) {
// 无需处理,已存在就直接用即可
}
}
}
tryLock()逻辑:
@Override
public boolean tryLock() {
if (this.reentrantCount.get() != null) {
int count = this.reentrantCount.get();
if (count > 0) {
this.reentrantCount.set(++count);
return true;
}
}
if (this.currentPath.get() == null) {
// 创建临时顺序节点,zk中创建节点必须赋值,所以这里随便赋值aaa
currentPath.set(this.client.createEphemeralSequential(lockPath + "/", "aaa"));
}
// 获得所有的子
List<String> children = this.client.getChildren(lockPath);
// 排序list
Collections.sort(children);
// 判断当前节点是否是最小的
if (currentPath.get().equals(lockPath + "/" + children.get(0))) {
this.reentrantCount.set(1);
return true;
} else {
// 取到前一个
// 得到字节的索引号
int curIndex = children.indexOf(currentPath.get().substring(lockPath.length() + 1));
beforePath.set(lockPath + "/" + children.get(curIndex - 1));
}
return false;
}
lock()逻辑:
@Override
public void lock() {
if (!tryLock()) {
// 阻塞等待
waitForLock();
// 再次尝试加锁
lock();
}
}
private void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
// 注册watcher
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println("-----监听到节点被删除----");
cdl.countDown();
}
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};
client.subscribeDataChanges(this.beforePath.get(), listener);
if (this.client.exists(this.beforePath.get())) {
try {
// 阻塞
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 醒来后,取消watcher
client.unsubscribeDataChanges(this.beforePath.get(), listener);
}
unlock()逻辑
@Override
public void unlock() {
// 重入的释放锁处理
if (this.reentrantCount.get() != null) {
int count = this.reentrantCount.get();
if (count > 1) {
this.reentrantCount.set(--count);
return;
} else {
this.reentrantCount.set(null);
}
}
// 删除当前节点
this.client.delete(this.currentPath.get());
}
测试:
public static void main(String[] args) {
// 并发数
int currency = 50;
// 利用CyclicBarrier模拟并发场景,CyclicBarrier的作用是集满指定线程个数,一起发出去
CyclicBarrier cb = new CyclicBarrier(currency);
for (int i = 0; i < currency; i++) {
new Thread(new Runnable() {
public void run() { System.out.println(Thread.currentThread().getName() + "---------Ready---------------");
// 等待一起出发
try {
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
ZKDistributedLock lock = new ZKDistributedLock("/distLock");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 获得锁!");
} finally {
lock.unlock();
}
}
}).start();
}
}
如果使用zookeeper原生方式来用代码实现分布式锁的话还是比较繁琐的,日常开发中可以使用Apache的开源客户端Curator封装好的分布式锁。
推荐一篇看到的好文:
Curator实现分布式锁的基本原理
五、docker安装zk
随便找了一个,可以在自己虚拟机上试试命令行
docker pull zookeeper
docker run --privileged=true -d --name zookeeper --publish 2181:2181 -d zookeeper:latest
有需要也可以自行安装window、mac版本的zookeeper
本文介绍了Zookeeper作为分布式协调服务的基础,包括核心概念如Session和数据模型,特别是分布式锁的实现原理,如通过znode节点命名唯一和临时顺序节点结合watch机制。还提供了Java代码示例,并探讨了不同实现方式的适用场景,以及如何通过Docker安装Zookeeper。
172万+





