文章目录
- 前言
- 介绍
- 安装与启动
- 配置说明
- 节点
- 节点类型
- PERSISTENT(持久化节点)
- PERSISTENT_SEQUENTIAL(持久化顺序节点)
- EPHEMERAL(临时节点)
- EPHEMERAL_SEQUENTIAL(临时顺序节点)
- Container(容器节点)
- PERSISTENT_WITH_TTL(持久化TTL节点)
- PERSISTENT_SEQUENTIAL_WITH_TTL(持久化TTL顺序节点)
- 基本操作
- 创建节点
- 设置数据
- 获取节点数据
- 列出节点
- 状态
- 删除
- 监听
- 权限(ACL)
- 设置world权限
- 设置auth权限
- 设置digest权限
- 设置ip权限
- 设置super权限
- 原生客户端
- 监听场景
- curator
- NodeCache
- PathChildrenCache
- TreeCache
- 集群
- 分布式锁
- 非公平锁
- 公平锁
前言
本篇旨在了解zookeeper和使用它,因为我也是刚开始接触,所以没有过度深入。
为什么学习它,zookeeper作为分布式协调系统,应用于springCloud ,dubbo,kafka,Hadoop等,算是比较主色的一个系统。
介绍
首页zookeeper是分布式协调框架,主要用来解决分布式应用中经常遇到的数据管理问题,如:统一命名服务、状态同步、集群管理、分布式应用配置项的管理等。
安装与启动
-
下载:https://dlcdn.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz
-
解压:tar -xf
-
bin目录设置成环境变量
vi ~/.bash_profile
export ZOOKEEPER_HOME=/data/zk/apache-zookeeper-3.6.3-bin
export PATH= P A T H : PATH: PATH:ZOOKEEPER_HOME/bin -
复制zoo_sample.cfg -> zoo.cfg;zoo.cfg才是启动配置
-
启动zookeeper服务端,出现【started】就是启动成功了
./bin/zkServer.sh start
-
启动zookeeper客户端,没有报错就是连上了
./bin/zkCli.sh
配置说明
zoo.cfg
# 心跳时间间隔,单位毫秒
tickTime=2000
# 数据同步最长时间 10*2=20秒
initLimit=10
# 心跳检查时的存活时间,超过这个时间,判断为不存活
syncLimit=5
# 持久化配置
dataDir=/tmp/zookeeper
# 客户端端口
clientPort=2181
# 客户端连接最大数量
#maxClientCnxns=60
# 持久化目录保存的快照数
#autopurge.snapRetainCount=3
# 清除多余日志和快照的时间间隔,=0时禁用
#autopurge.purgeInterval=1
## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true
节点
它维护着一个类似文件系统的数据结构,每一个子目录项被称为znode(目录节点)
节点类型
PERSISTENT(持久化节点)
客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在
PERSISTENT_SEQUENTIAL(持久化顺序节点)
客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行顺序编号
EPHEMERAL(临时节点)
客户端与zookeeper断开连接后,该节点被删除。
- 临时节点下不能拥有子节点
- 其他客户端也能看到临时节点
EPHEMERAL_SEQUENTIAL(临时顺序节点)
客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行顺序编号
Container(容器节点)
3.5.3 版本新增,如果Container节点下面没有子节点,则Container节点在未来会被zookeeper自动清除,定时任务默认60s 检查一次
PERSISTENT_WITH_TTL(持久化TTL节点)
3.5.3 版本新增,默认禁用,只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启,拥有一个过期时间
PERSISTENT_SEQUENTIAL_WITH_TTL(持久化TTL顺序节点)
3.5.3 版本新增,默认禁用,只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启,在顺序节点的基础上增加了过期时间概念
基本操作
连接后,可以进行创建节点、修改、删除节点和数据,详细的命令它都有提示(随便输入一个就会打印提示)
zookeeper的数据结构是类似文件系统的数据结构,所以,它的节点可以看做是一个目录,比如/test
,作为一个节点,需要一个/
作为路径前缀,也表示他是根节点下名字叫test的节点,同理,子节点也是一样,如/test/t
,表示根节点下名为test的节点下有一个节点t
。
创建节点
#创建名为test的znode节点
create /test
#查看节点列表
ls /
设置数据
#设置了节点为test的数据为xxx
set /test xxx
获取节点数据
#获取节点为test的数据
get /test
#获取节点详细信息
get -s /test
列出节点
ls /
状态
stat /test
删除
delete /test
#删除所有的节点,保护子节点
deleteall /test
监听
这种方式的监听是一次性的,只能监听一次。
#开启一个新的客户端
get -w /test
#通过另一个客户端修改/test数据
set /test sdsds
权限(ACL)
- zookeeper的权限是基于节点的, 所以需要对每个节点做权限
- 每个znode支持设置多种权限控制方案和多个权限
- 子节点不会接触父节点的权限
zookeeper的权限构成:scheme🆔permissions
- scheme: 代表权限机制,包括world、auth、digest、ip、super几种
- id: 代表允许访问的用户
- permissions:权限组合字符串,有cdrwa组成;create(c),delete(d),read(r),write(w),admin(a)
之前创建的节点,都是下面的权限,也是默认的权限。
'world,'anyone
- cdrwa
设置world权限
#创建一个子节点
create /test/t
#查看权限
getAcl /test/t
#设置权限只为创建读取
setAcl /test/t world:anyone:cr
尝试写入数据
set /test/t sssss
设置auth权限
首先创建用户
addauth digest ali:123456
然后创建一个指定用户,且只有读写的权限的节点
create /test/tt xxx auth:ali:123456:cwr
然后再开一个客户端来验证
设置digest权限
这个需要密码加密,所以使用下面的命令加密
#使用sha1算法加密后,再用base64加密
echo -n ali:123456 | openssl dgst -binary -sha1 | openssl base64
得到加密密码后,进行权限设置,统一只有读写权限
create /test/ttt xsxs digest:ali:sEcavS+dPUZVZKgy6MHOOj/AKTs=:cwr
再使用另一个客户端查看
登录再操作,发现已经可以操作了。
addauth digest ali:123456
设置ip权限
限制操作的ip
create /test/t2 tt2x ip:192.168.0.110:cwr
通过另一台服务器尝试连接
./bin/zkCli.sh -server 192.168.0.110:2181
可是,在通过其他服务器连接时,出现下面报错:
经过排查,是因为我zookeeper所在的服务器,没有把端口2181开放
firewall-cmd --add-port=2181/tcp --permanent
设置super权限
admin权限拥有所有的权限,在出现没有权限操作的节点时,可以通过admin权限进行操作。
先生成秘钥
echo -n super:admin | openssl dgst -binary -sha1 | openssl base64
在zkServer.sh脚本正添加启动参数,如下(秘钥前面添加账号名):
-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=
创建一个只有读权限的节点
通过另一个客户端,登录super
addauth digest super:admin
可以看到,即使没有写权限,admin登录的仍然可以修改。
注意点:设置了删除权限,但是还是可以用delete命令操作,感觉权限是针对deleteall的,并不适用delete命令
原生客户端
zookeeper有两个客户端;其一便是上面我们使用的zkCli.sh,其二就是Java客户端。
private static final String CONNECT_STR = "192.168.0.110:2181";
private static final int TIME_OUT = 30000;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
final CountDownLatch latch = new CountDownLatch(1);
Watcher startWatcher = new Watcher() {
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("建立连接");
latch.countDown();
}
String path = watchedEvent.getPath();
System.out.println("path:" + path);
}
};
ZooKeeper zookeeper = new ZooKeeper(CONNECT_STR, TIME_OUT, startWatcher);
// zookeeper内部有开启子线程,所以这里通过countlatch锁阻止主线程的执行
latch.await();
// 判断节点/test是否存在
Stat exists = zookeeper.exists("/test", false);
// 当节点不存在时exits=null
System.out.println(exists);
// 获取节点/test数据
// 查询节点,需要一个stat来承载节点信息
Stat stat = new Stat();
byte[] data = zookeeper.getData("/test", false, stat);
System.out.println("节点/test数据:");
System.out.println(new String(data));
// 修改节点/test数据
// version=-1表示不检查版本,乐观锁的实现,可以通过stat拿到version(stat.getVersion())
zookeeper.setData("/test", "false".getBytes(), -1);
data = zookeeper.getData("/test", false, stat);
System.out.println("节点/test修改后的数据:");
System.out.println(new String(data));
// 创建节点/test/ttttt
ZkData d = new ZkData();
d.setKey("ddd");
d.setValue("vvv");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValueAsBytes(d);
String s = zookeeper.create("/test/tttt", objectMapper.writeValueAsBytes(d), ZooDefs.Ids
.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
data = zookeeper.getData("/test/tttt", false, stat);
System.out.println("节点/test/tttt的数据:");
System.out.println(new String(data));
}
监听场景
Watcher dataWatch = new Watcher() {
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDataChanged) {
System.out.println("数据更新");
System.out.println(event);
// 重新获取一次数据getData
try {
byte[] data1 = zookeeper.getData("/test", false, null);
System.out.println(new String(data1));
} catch (KeeperException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
byte[] data1 = zookeeper.getData("/test", dataWatch, null);
System.out.println();
// 这里阻止zookeeper监听线程结束
TimeUnit.SECONDS.sleep(600);
在命令行客户端修改/test节点数据,java客户端这边收到change事件
curator
private static final String CONNECT_STR = "192.168.0.110:2181";
private static final int TIME_OUT = 30000;
public static void main(String[] args) throws Exception {
// 重试配置
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(CONNECT_STR)
.sessionTimeoutMs(TIME_OUT)
.connectionTimeoutMs(TIME_OUT)
.retryPolicy(retryPolicy)
.build();
client.start();
// 创建节点
String path = client.create().forPath("/test2", "xxx".getBytes());
System.out.println(path);
// 查询节点数据
byte[] bytes = client.getData().forPath("/test2");
System.out.println("获取到的数据:" + new String(bytes));
// 修改节点数据
Stat stat = client.setData().forPath("/test2", "777".getBytes());
bytes = client.getData().forPath("/test2");
System.out.println("修改后的数据:" + new String(bytes));
// 删除节点
client.delete().forPath("/test2");
// 节点不存在,这里报异常
bytes = client.getData().forPath("/test2");
System.out.println("删除后的数据:" + new String(bytes));
}
NodeCache
监听当前节点数据的变化
private static final String CONNECT_STR = "192.168.0.110:2181";
private static final int TIME_OUT = 30000;
public static void main(String[] args) throws Exception {
// 重试配置
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(CONNECT_STR)
.sessionTimeoutMs(TIME_OUT)
.connectionTimeoutMs(TIME_OUT)
.retryPolicy(retryPolicy)
.build();
client.start();
NodeCache nodeCache = new NodeCache(client, "/test2");
nodeCache.start();
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("数据变更");
ChildData currentData = nodeCache.getCurrentData();
System.out.println(new String(currentData.getData()));
}
});
TimeUnit.SECONDS.sleep(6000);
}
每次在变更后,都会被监听到
PathChildrenCache
监听子节点
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/test2", true);
pathChildrenCache.start();
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent)
throws Exception {
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
System.out.println("新增子节点");
System.out.println("新增节点的数据:" + new String(pathChildrenCacheEvent.getData().getData()));
break;
case CHILD_UPDATED:
System.out.println("更新子节点");
System.out.println("更新节点的数据:" + new String(pathChildrenCacheEvent.getData().getData()));
break;
default:break;
}
}
});
TimeUnit.SECONDS.sleep(6000);
TreeCache
监听当前节点和子节点
TreeCache treeCache = new TreeCache(client, "/test2");
treeCache.start();
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
switch (treeCacheEvent.getType()) {
case NODE_UPDATED:
System.out.println("更新节点");
System.out.println("更新节点的数据:" + new String(treeCacheEvent.getData().getData()));
break;
default:break;
}
}
});
TimeUnit.SECONDS.sleep(6000);
集群
我这里单机搭建集群,有些步骤也差不多的。
-
准备目录
mkdir /data/zk/data mkdir /data/zk/data2 mkdir /data/zk/data3 mkdir /data/zk/logs mkdir /data/zk/logs2 mkdir /data/zk/logs3
-
修改zoo.cfg,因为我单机,所以需要改动端口,和目录等
# 心跳时间间隔,单位毫秒
tickTime=2000
# 数据同步最长时间 10*2=20秒
initLimit=10
# 心跳检查时的存活时间,超过这个时间,判断为不存活
syncLimit=5
# 持久化配置
dataDir=/data/zk/data
dataLogDir=/data/zk/logs
# 客户端端口
clientPort=2181
# 集群配置
# server.x 这个x代表data/myid里的值,localhost是zookeeper服务坐在的ip地址,第一个端口是通信端口,第二个端口是选举端口
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
复制配置zoo2.cfg
# 心跳时间间隔,单位毫秒
tickTime=2000
# 数据同步最长时间 10*2=20秒
initLimit=10
# 心跳检查时的存活时间,超过这个时间,判断为不存活
syncLimit=5
# 持久化配置
dataDir=/data/zk/data2
dataLogDir=/data/zk/logs2
# 客户端端口
clientPort=2182
# 集群配置
# server.x 这个x代表data/myid里的值,localhost是zookeeper服务坐在的ip地址,第一个端口是通信端口,第二个端口是选举端口
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
zoo3.cfg
# 心跳时间间隔,单位毫秒
tickTime=2000
# 数据同步最长时间 10*2=20秒
initLimit=10
# 心跳检查时的存活时间,超过这个时间,判断为不存活
syncLimit=5
# 持久化配置
dataDir=/data/zk/data3
dataLogDir=/data/zk/logs3
# 客户端端口
clientPort=2183
# 集群配置
# server.x 这个x代表data/myid里的值,localhost是zookeeper服务坐在的ip地址,第一个端口是通信端口,第二个端口是选举端口
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
-
创建myid
# 在每个data下创建myid文件,值分别是1,2,3,对应zoo.cfg配置文件最后的server.x后缀 vi /data/zk/data/myid vi /data/zk/data2/myid vi /data/zk/data3/myid
-
启动
启动器,需要全部的zookeeper服务都是关闭的。
./bin/zkServer.sh start conf/zoo.cfg ./bin/zkServer.sh start conf/zoo2.cfg ./bin/zkServer.sh start conf/zoo3.cfg
-
查看服务状态
./bin/zkServer.sh status conf/zoo.cfg ./bin/zkServer.sh status conf/zoo2.cfg ./bin/zkServer.sh status conf/zoo3.cfg
-
操作测试
连接3个客户端,尝试,创建一个节点,活修改一个节点数据,可以发现已经同步了。
分布式锁
代码仓库:https://gitee.com/LIRUIYI/test-zk.git
非公平锁
通过创建节点,并判断节点是否存在实现分布式锁。
- 创建临时节点,如
/lock
,临时节点是当出现异常,没有删除导致永久加锁的情况发生 - 当节点不存在,创建节点成功,也就意味着枷锁成功,如果创建失败,那么就是加锁失败
- 其他需要加锁的线程,监听
/lock
节点(get -w /lock),当节点/lock
删除后,zookeeper会通知到监听的节点
这种方式的加锁,不能保证需要加锁的线程能得到锁的概率一样,他们是随机的,有可能最先排队的加锁线程,到最后都不能得到锁,这就是非公平锁。
以原始客户端实现非公平锁:
这里zookeeper地址和上面不一样,因为之前是桥接模式,自动获取的,有时ip会变动,所以改NAT模式,设定了静态ip
package com.liry.zk;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
/**
* 分布式锁 - 非公平锁实现
*
* @author ALI
* @since 2022/12/25
*/
@Slf4j
public class NonfairSyncLock {
private static final String CONNECT_STR = "192.168.17.128:2181";
private static final int TIME_OUT = 30000;
private static final String LOCK_PATH = "/lock";
private static ZooKeeper zookeeper = null;
/**
* 获取客户端
*/
public static ZooKeeper getClient() throws IOException, InterruptedException {
synchronized (LOCK_PATH) {
final CountDownLatch latch = new CountDownLatch(1);
if (zookeeper == null) {
Watcher startWatcher = watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
log.info("与zookeeper建立连接");
latch.countDown();
}
};
zookeeper = new ZooKeeper(CONNECT_STR, TIME_OUT, startWatcher);
latch.await();
} else if (zookeeper.getState() != ZooKeeper.States.CONNECTED) {
latch.await();
}
}
return zookeeper;
}
/**
* 加锁
*/
public void lock() {
while (true) {
if (tryLock()) {
return;
}
// 加锁失败,增加监听,然后阻塞
try {
watch();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 解锁
*/
public void unlock() {
try {
getClient().delete(LOCK_PATH, -1);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 尝试加锁
*/
private boolean tryLock() {
try {
getClient().create(LOCK_PATH, "lock".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
} catch (Exception e) {
log.error("加锁失败");
return false;
}
return true;
}
/**
* 监听锁节点
* 当节点存在时,线程阻塞,当节点不存在,直接退出监听
*/
private void watch() throws InterruptedException, KeeperException, IOException {
CountDownLatch latch = new CountDownLatch(1);
Watcher dataWatch = event -> {
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
latch.countDown();
}
};
try {
getClient().getData(LOCK_PATH, dataWatch, null);
} catch (KeeperException.NoNodeException e) {
// 当创建监听时,节点不存在,说明有线程解锁了,那么直接退出,监听步骤,去争抢锁
return;
}
latch.await();
}
}
static int count = 0;
public static void main(String[] args) throws InterruptedException {
nonFairLock();
}
private static void nonFairLock() throws InterruptedException {
NonfairSyncLock lock = new NonfairSyncLock();
CountDownLatch latch = new CountDownLatch(1000);
List<Thread> threadList = IntStream.range(0, 1000).mapToObj(d -> new Thread(() -> {
lock.lock();
count += 1;
latch.countDown();
lock.unlock();
}, "线程-" + d)).collect(Collectors.toList());
threadList.forEach(Thread::start);
latch.await();
System.out.println("最终结果应是1000:" + count);
}
公平锁
相对于非公平锁的实现,这个方式较为复杂一点。
-
先创建一个根节点
/lock
-
再在
/lock
下创建临时有序节点,有序节点是因为所有需要加锁的节点需要按先来后到的顺序才能公平 -
然后每个有序节点都监听它的前一个节点,如图,当前一个节点被删除,表示解锁了,那么zookeeper会通知到监听的节点,也就是下一个需要加锁的线程
这个在curator中已经有实现了,分布式锁不局限于zookeeper,了解其原理就行
static int count = 0;
public static void main(String[] args) throws InterruptedException {
// nonFairLock();
fairLock();
}
private static void fairLock() throws InterruptedException {
// 使用curator客户端
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("92.168.17.128:2181")
.sessionTimeoutMs(3000)
.connectionTimeoutMs(3000)
.retryPolicy(retryPolicy)
.build();
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/lock");
CountDownLatch latch = new CountDownLatch(1000);
List<Thread> threadList = IntStream.range(0, 1000).mapToObj(d -> new Thread(() -> {
try {
lock.acquire();
} catch (Exception e) {
throw new RuntimeException(e);
}
count += 1;
latch.countDown();
try {
lock.release();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "线程-" + d)).collect(Collectors.toList());
threadList.forEach(Thread::start);
latch.await();
System.out.println("最终结果应是1000:" + count);
}