基本属性
- zk是一个树形目录服务。
- zk是一个分布式应用中心协调服务
- 默认端口2181
- 常用的几个功能:
- 配置管理:第三方配置中心
- 分布式锁:第三方分布式锁管理
- 集群管理:服务管理和通信
数据模型
- 树形目录的每个节点都被称为ZNode,每个接地那都会保存自己的数据和节点信息。
- 节点可以拥有子节点,允许少量(1MB)数据存储在该节点下。
- 节点分为四大类:
- PERSISTENT 持久化节点,即会长久保存的接地那
- EPHEMERAL临时接地那:-e,例如客户端建立的短暂会话
- PERSISTENT_SEQUENTIAL持久化顺序节点:-s,有顺序的节点,后缀会有_1 _2等
- EPHEMERAL_SEQUENTIAL临时顺序节点:-es
ZK客户端工具client
- 链接zk server:
./bin./zkCli.sh -server ip:port
- 查看zk根路径下节点:
ls /
- 创建app1,并指定数据为speeder:
create /app1 speeder
- 获取数据:使用
get /app1
命令,如果作为注册中心,获取节点的数据信息中会有providers、consumers等,消费者也是根据这些数据进行远程调用。 - 修改数据:
set /app1 speeder-test
- 删除节点:
delete /app1
- 创建临时节点(默认创建的是永久节点,临时节点如果客户端关掉之后,临时节点就关掉了):
create -e /app2
- 创建临时有序节点:
create -es
- 查询节点详细信息:
ls -s /节点路径
得到的结果说明如下:- czxid:节点被创建的事务ID
- ctime:创建时间
- mzxid:最后一次被更新的事务ID
- mtime:修改时间
- pzxid:子节点列表最后一次被更新的事务ID
- cversion:子节点的版本号
- dataversion:数据版本号
- aclversion:权限版本号
- ephemeralOwner:用于临时节点,表示临时节点的事务ID,如果是持久化节点则为0
- dataLength:节点存储的数据长度
- numChildren:当前节点的子节点个数
ZK的java api操作(curator)
- Curator:zk的java客户端,简化了zk客户端操作。(curator翻译的意思是园长,开发者别有用心,官压zk动物园看守员好多级)
- curator api的建立连接、以及增删改查参考其他博客:点击跳转。
- 但是修改操作中,需要注意的是,需要使用CAS乐观锁保证修改数据的幂等性和原子性,在数据详细信息中,有
dataVersion
属性,表示数据的版本信息,每一次修改后,都会自增1,就可以每次更新时,先使用getVersion()
方法获取dataVersion,然后再使用withVersion(version)
方法修改数据。 - 在删除操作中,如果想排除网络通信或者其他因素的干扰,可以使用
gurantted()
方法,确保如果删除失败后,可以重试。另外删除成功后可以有手工回调函数。
- 但是修改操作中,需要注意的是,需要使用CAS乐观锁保证修改数据的幂等性和原子性,在数据详细信息中,有
Watch事件监听
- ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
- ZooKeeper 中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
- ZooKeeper 原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便
需要开发人员自己反复注册Watcher,比较繁琐。 - Curator引入了 Cache 来实现对 ZooKeeper 服务端事件的监听。
- ZooKeeper提供了三种Watcher:
- NodeCache : 只是监听某一个特定的节点
- PathChildrenCache : 监控一个ZNode的子节点,注意是只监控自己的子节点,自己节点变化是不监控的
- TreeCache : 可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合,既监控自己,也监控子节点。
写写代码,Curator api操作:
- NodeCache,只是监听某一个特定的节点
// 1、创建监听对象
NodeCache nodeCache = new NodeCache(client, "/test");
// 2、设置监听器
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("节点发生改变");
try {
byte[] bytes = nodeCache.getClient().getData().forPath(nodeCache.getPath());
System.out.println(new String(bytes));
}catch (Exception e){
System.out.println("节点异常,可能已经删除");
e.printStackTrace();
}
}
});
// 3、开启
nodeCache.start();
- PathChildrenCache,监控一个ZNode的子节点.
// 1、创建监听对象
PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"/test1",true);
// 2、绑定个监听器
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
System.out.println("节点发生了改变");
// 如果变更,才打印
if(pathChildrenCacheEvent.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
byte[] data = pathChildrenCacheEvent.getData().getData();
System.out.println(new String(data));
}
}
});
// 3、开启
pathChildrenCache.start();
- TreeCache : 可以监控整个树上的所有节点
TreeCache treeCache = new TreeCache(client,"/test1");
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
System.out.println("节点改变了");
if (treeCacheEvent.getType().equals(TreeCacheEvent.Type.NODE_UPDATED)){
System.out.println(new String(treeCacheEvent.getData().getData()));
}
}
});
treeCache.start();
分布式锁
分布式锁实现的常见方式:
- 基于缓存实现分布式锁:redis、memcache
- zk实现分布式锁:Curator
- 数据库层面实现分布式锁:悲观锁、乐观锁
Zookeeper分布式锁的实现原理:
- 作为第三方分布式锁管理中心,其核心设计思路和redis差不太多,redis是根据key存不存在判断有没有加锁,而zk是客户端想要获取锁的时候,创建节点ZNode,使用完锁之后,删除该Znode。加锁和获取锁的大致过程:
- 客户端获加锁时,在zk的某个节点(这些节点要统筹规划,最好形成业务壁垒)下创建
临时顺序
节点。例如有三个客户端client,分别在lock节点下分别创建了三个临时顺序子节点ZNode1、Znode2、znode3。为什么强调是临时顺序的?- 临时:因为根据分布式锁的设计机制,各个客户端在使用完资源后,一定要释放该资源,在zk里也就是删除加锁时创建的节点,不然就一直占据着锁资源了。但是!万一客户端加完锁之后就宕机了,那不就永久无法释放资源了吗,所以就需要使用临时节点,因为运用到了临时节点的特性,即客户端断开之后,临时节点自动删除。
- 顺序:依我理解,在这里要形成对资源进行排队加锁,形成客户端队列,以有序的方式对资源进行占用,先到先得。而zk中的顺序队列特性刚好满足此场景,即后创建的节点顺序越靠后。
- 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,也就是排队排到自己了,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
- 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,注意是只需要监控自己前边的那个顺序节点,同时对其注册事件监听器,监听删除事件。
- 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
Curator几种锁的方案
- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
- InterProcessMutex:分布式可重入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
模拟抢票场景,进行分布式加锁:
public class BookTicket implements Runnable{
private int tickets = 100;// 票数
private InterProcessMutex lock ;
public BookTicket(){
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
//2.第二种方式
//CuratorFrameworkFactory.builder();
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.build();
//开启连接
client.start();
lock = new InterProcessMutex(client,"/lock");
}
@Override
public void run() {
while(true){
//获取锁
try {
lock.acquire(3, TimeUnit.SECONDS);
if(tickets > 0){
System.out.println(Thread.currentThread()+":"+tickets);
Thread.sleep(100);
tickets--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
zk集群搭建
集群中Leader选举:(leader/follower)
- Serverid:服务器ID
比如有三台服务器,编号分别是1,2,3。
编号越大在选择算法中的权重越大。 - Zxid:数据ID
服务器中存放的最大数据ID.值越大说明数据 越新,在选举算法中数据越新权重越大。 - 在Leader选举的过程中,如果某台ZooKeeper
获得了超过半数的选票,
则此ZooKeeper就可以成为Leader了。
zk集群配置(zk单节点正常搭建完毕后,配置集群):
- 在每台zk的data目录下创建一个myid文件,分别记录服务器ID,即Serverid
echo 1 >/app/zk-cluster/zk-1/data/myid
echo 2 >/app/zk-cluster/zk-2/data/myid
- 在每台zk的zoo.cfg配置客户端访问端口(clientport)和集群服务器IP列表。
vim /app/zk-cluster/zk-1/conf/zoo.cfg
vim /app/zk-cluster/zk-2/conf/zoo.cfg
server.1=localhost:2881:3881
server.1=localhost:2882:3882
注意:各位是server.服务器ID=服务器IP:服务节点之间通信端口:服务节点之间投票选举端口
- 每台zk分别启动即可
./bin/zkserver.sh start
ps:集群中常用的主节点、从节点的词汇对:
- master / slave 主从集群,但是随着种族歧视问题被重视,很多国际公司不愿意使用slave类似词语,更倾向于接来下后两种叫法。
- primary / standby
- leader / follower
zk集群中的角色
- leader领导者:
- 处理事务请求,事务请求就是指增、删、改的操作,没有查。
- 集群内部个服务器的调度者,由于事务请求只能leader做,而且事务请求会对数据做出修改,所以leader修改完数据后,需要将数据同步给各个节点,包括Observer观察者。
- Follower跟随者:
- 处理非事务请求,转发事务请求给leaderr处理。
- 参与Leader选举投票。
- Observer观察者:
- 处理非事务请求,转发事务请求给leaderr处理,分担follower的压力。