zookeeper 重要概念介绍
ZooKeeper
是 Apache
软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册,是一个典型的 分布式数据一致性解决方案 。
目标:ZooKeeper
致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的 分布式协调服务 。
- 高性能:将 全量数据存储在内存 中,并直接服务于客户端的所有非事务请求,尤其适用于 以读为主 的应用场景,对于事务请求操作多的不太适合。
- 高可用:一般以集群的方式对外提供服务,只要集群中超过一般的机器都能够正常工作,那么整个集群就能够正常对外服务。
- 严格的访问顺序:对于来自客户端的每个事务请求,
zk
都会分配一个全局事务id:zxid
,它反映了所有事务操作的先后顺序。
zk 集群角色
Leader
:集群工作的核心,事务请求的唯一调度和处理者,保证事务处理的顺序性。对于有写操作的请求,需统一转发给Leader
处理。Leader
需决定编号执行操作。Follower
:处理客户端非事务请求,转发事务请求转发给Leader
,参与Leader
选举。Observer
观察者:处理非事务请求的独立处理,对于事务请求, 同样转发给Leader
服务器进行处理,不参与leader
的选举,不参与2PC
数据同步过程。
2PC
数据同步过程具体的内容可以参考ZK
的数据同步流程章节的 消息广播 部分
zk 节点的特性
同一级节点 key
名称唯一
已经存在的节点再次创建便会提示节点已经存在。
创建节点时,必须指定节点的全路径
$ ls /runoob
$ create /runoob/child 0
$ create /runoob/child/ch01 0
临时节点在 session
关闭的时候就会被清除
自动给顺序节点进行编号
# 创建顺序节点 create -s 就是会在目录的后面添加一串序列号
$ create /aha
Created /aha0000000003
提供了节点的 watch
机制
参考下面章节的:演示 zkClient
的监听事件
delete
命令只能一层一层的删除
当使用 delete
命令删除具有子节点的节点时,会报错。
可以使用 deleteall
命令进行实现
zk 的 权限控制
zk
的 ACL(Access Control List)
即访问控制列表,权限在生产环境中还是很重要的一环。
ACL 的构成
zk
的 acl
通过 [scheme:id:permissions]
来定义权限列表
-
scheme
:代表采用的某种权限机制,包括world、auth、digest、ip
这几种。world
:默认方式,相当于全世界都能访问auth
:代表已经认证通过的用户digest
:使用用户名和密码的方式进行认证ip
:使用ip
地址的方式进行认证,可以指定具体的ip
或者 地址段
-
id
:代表允许访问的用户。 -
permissions
:权限组合字符串,由cdrwa
组成,其中每个字母代表支持不同权限,创建权限create(c)
、删除权限delete(d)
、读权限read(r)
、写权限write(w)
、管理权限admin(a)
,管理员权限是允许进行ACL
的操作,其中d
是对子节点的操作权限,其他的都是对自身节点的操作权限。
使用 digest
模式进行权限控制
编写工具类生成创建 digest
模式用户所需要的用户名和密码
导入 maven
依赖
<!-- 创建 Zookeeper 客户端依赖,一定要和 Zookeeper Server 版本保持一致 curator依赖中其实也包含,但是这边为了版本
对应显示的声明了一下-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
<exclusions>
<!--因为 zk 包使用的是 log4j 日志,和 springboot 的logback 日志冲突 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
编写工具类
package com.aha.utils;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import java.security.NoSuchAlgorithmException;
/**
* zk 的工具类
*
* @author: WT
* @date: 2021/12/1 16:36
*/
@Slf4j
public class ZkUtils {
/**
* 生成 digest 用户名和密码的加密串
* @param idPassword 用户名和密码的明文
* @return
* @throws NoSuchAlgorithmException
*/
public static String getDigestAuthStr (String idPassword) throws NoSuchAlgorithmException {
return DigestAuthenticationProvider.generateDigest(idPassword);
}
public static void main(String[] args) throws NoSuchAlgorithmException {
// user:svY1VPBeOtL797NihAvQMUtFLHs=
log.info(ZkUtils.getDigestAuthStr("user:111111"));
}
}
根据生成的加密权限串对相应的节点进行设置
# 创建 aha 节点,并设置 user:111111 用户拥有 读写和删除权限和设置acl权限
$ create /aha aha-data digest:user:svY1VPBeOtL797NihAvQMUtFLHs=:rwda
# 执行命令之后在不进行用户登录时在查看 /aha 节点
$ ls /aha
# 返回权限不足
[zk: localhost:2181(CONNECTED) 2] get /aha
Insufficient permission : /aha
# 进行用户的登陆 然后在查看相应的节点
$ addauth digest user:111111
$ get /aha
# 就可以正常的进行查看了
[zk: localhost:2181(CONNECTED) 7] get /aha
aha-data
采坑指南:
# 创建 aha 节点,并设置 user:111111 用户拥有 读写和删除权限和设置acl权限
$ create /aha aha-data digest:user:svY1VPBeOtL797NihAvQMUtFLHs=:rwda
在执行这个命令的时候,一定要给节点设置值,因为 create
命令是这个样子的:create [-s] [-e] [-c] [-t ttl] path [data] [acl]
所以如果不添加 data
就想添加 acl
的话,acl
的串会被当成 data
设置到节点中,不会设置 acl
。
使用 auth
模式进行权限的设置
auth
模式与 digest
模式的区别为:
- 在使用
auth
模式设置节点的权限的时候,需要先进行用户的创建,即要先进行addauth digest user:pwd
- 再给节点进行权限设置的时候只需要用户名设置进去就可以了,参考下面的实例
# 添加用户
$ addauth digest aha:111
# 添加节点并设置权限
$ create /auth auth-data auth:aha:cdwra
# 在其他客户端连接或者是本客户端重连之后想要操作 /auth 就需要先进行用户的登陆,否则就会报权限不足
$ addauth digest aha:111
# 之后就可以进行操作了
使用 ip
模式进行权限的设置
# 指定 10.211.55.4 有 cdrwa 权限
$ create /ip ip-date ip:10.211.55.4:cdrwa
# 不是 10.211.55.4 对 /ip 节点进行操作
$ get /ip
# 提示权限不够足
[zk: localhost:2181(CONNECTED) 0] ls /ip
Insufficient permission : /ip
# 使用 10.211.55.4 对 /ip 节点进行操作
$ get /ip
# 可以正常返回
# 使用多个 ip 的方式
$ setAcl /ip ip:10.211.55.4:cdwra,ip:10.211.55.5:cdwra
# 也可以使用 ip 段的方式
$ setAcl /ip ip:10.211.55.4/24:cdwra
zk 的读写机制
zk
使用单一的主进程 Leader
来接收和处理客户端所有事务请求,注意这里是事务请求,非事务请求可以直接通过 follower
或 observer
进行操作。
所谓事务请求便是涉及到数据修改的请求:新增,修改,删除
所谓非事务请求便是:查询操作
当非 leader
节点接受到了事务操作,它会将请求转发给 leader
节点来处理。
zk 的数据同步流程
在 zk
中主要依赖 ZAB:(Zookeeper Atomic Broadcas)
原子消息广播协议来实现分布式数据的一致性。
ZAB
协议与 Paxos
类似,也是一种数据一致性的算法,他主要包括:消息广播 和 崩溃恢复 两个部分。
消息广播
当节点收到客户端或者 observer,follower
节点转发过来的事务请求后,leader
节点会将此请求转化为 Proposal: (提议)
广播到所有的 follower
节点,注意这边不包括 observer
节点,当集群中有过半的 follower
节点给 leader
节点进行了正确的 ACK
反馈,这时候 leader
节点就会像所有的 follower
节点发送 commit
消息,将此次提议进行提交。这个过程可以被称为 2PC
事务提交。如下图所示:
注意:observer
节点只负责同步 leader
的数据,全程不参与 2PC
的过程。
ACK : Acknowledge character
确认字符
崩溃恢复
在正常情况消息广播情况下能运行良好,但是一旦 Leader
服务器出现崩溃,或者由于网络原理导致 Leader
服务器失去了与过半 Follower
的通信,那么就会进入崩溃恢复模式,需要选举出一个新的 Leader
服务器。在这个过程中可能会出现两种数据不一致性的隐患,需要 ZAB
协议的特性进行避免。
ZAB
协议的恢复模式使用了以下策略:
- 选举
zxid
最大的节点作为新的leader
- 新
leader
将事务日志中尚未提交的消息进行处理
zk leader 选举
重要参数介绍
- 服务器
ID(myid)
:编号越大在选举算法中权重越大 - 全局事务
ID(zxid)
:值越大说明数据越新,权重越大 - 逻辑时钟
(epoch-logicalclock)
:同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加,用来验证是否是同一轮投票
选举状态介绍
LOOKING
:竞选状态FOLLOWING
:随从状态,同步leader
状态,参与投票OBSERVING
:观察状态,同步leader
状态,不参与投票LEADING
:领导者状态
选举流程介绍
选举使用的算法便是 paxos
算法。
每个节点启动的时候都是 LOOKING
观望状态,接下来就开始进行选举流程。这里选取三台机器组成的集群为例。第一台服务器 server1
启动时,无法进行 leader
选举,当第二台服务器 server2
启动时,两台机器可以相互通信,进入 leader
选举过程。
-
发送投票:每台
server
发出一个投票,由于是初始情况,server1
和server2
都将自己作为leader
服务器进行投票,每次投票包含所推举的服务器myid
、zxid
、epoch
,这边使用servern(myid,zxid)
表示,此时server1
投票为(1,0)
,server2
投票为(2,0)
,然后将各自投票发送给集群中其他机器。 -
检查投票:集群中的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票
epoch
、是否来自LOOKING
状态的服务器。 -
投票对比:针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下:
- 优先比较
epoch
- 检查
zxid
,zxid
比较大的服务器优先作为leader
- 如果
zxid
相同,那么就比较myid
,myid
较大的服务器作为leader
服务器
- 优先比较
-
统计投票:每次投票后,服务器统计投票信息,判断是都有过半机器接收到相同的投票信息。
server1
、server2
都统计出集群中有两台机器接受了(2,0)
的投票信息,此时已经选出了server2
为leader
节点。 -
改变服务器状态。一旦确定了
leader
,每个服务器响应更新自己的状态,如果是follower
,那么就变更为FOLLOWING
,如果是Leader
,变更为LEADING
。此时server3
继续启动,直接加入变更自己为FOLLOWING
。
Zookeeper 的安装
环境介绍
- 操作系统:
Ubuntu 20.04.2 LTS
JDK 1.8.0_292
zookeeper-3.6.3
安装 JDK
Zookeeper
是 Java
语言开发的所以要先按照 JDK
,按照步骤可以参考:JDK 多平台安装
单节点方式安装 Zookeeper
节点规划
这边使用 master
机器进行单节点的演示,地址为 10.211.255.3
。
安装的目录为:/home/parallels/Downloads/zookeeper
。
去 Zookeeper
官方网站获取你想要版本的安装地址:https://zookeeper.apache.org/releases.html
。
注意:这边选择的时候不要选择源码包。
# 下载压缩包
$ wget https://dlcdn.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz
# 解压
$ tar -zxvf 安装包
# 我安装到了 /home/parallels/Downloads 下
$ cd conf
$ cp zoo_sample.cfg zoo.cfg
# 在 conf 目录下启动
$ ../bin/zkServer.sh start
# 查看服务端的启动状态
$ ../bin/zkServer.sh status
# 停止服务
$ ../bin/zkServer.sh stop
这样单节点 zk
的安装基本就搞定了。使用的配置文件就是默认的 conf/zoo.cfg
。
配置环境变量
配置环境变量就是为了能更方便的使用它的命令
# 编辑配置文件
$ vim /etc/environment
# 在 PATH 的前面添加
ZK_HOME="/home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin"
# 修改 PTAH,在它的最后面添加
:$ZK_HOME/bin
文件如下图所示:
使用 zkClient 操作 zkServer
在操作之前应该先保证 zkServer
的正常运行
基础操作
# 连接 zkServer
$ zkCli.sh -server
# 当不指定ip和端口的时候默认连接 127.0.0.1:2181,指定端口和ip示例如下
$ zkCli.sh -server 127.0.0.1:2181
# 连接成功之后 它的操作与 linux 文件系统的操作类似
$ ls /
# 创建目录并给目录赋值 默认是持久化节点
$ create /aha aha
# 查看创建目录的值
$ get /aha
# 在/aha 下创建子目录并且创建值
$ create /aha/child child
# 查看目录结构 ls /
# 下面会显示 aha 和 zookeeper 但是 是不能进入/aha 里面的 他是维护了一个虚拟的目录结构 是没有cd命令的 这边有一个 stat 命令是很重要的
$ stat /aha
# 返回以下信息
cZxid = 0x200000002 # 创建/aha 和这个对象的事务id
ctime = Wed May 12 13:25:46 CST 2021 # 创建节点时的时间
mZxid = 0x200000002 # 最后修改节点时的事务ID
mtime = Wed May 12 13:25:46 CST 2021 # 最后修改节点时的时间
pZxid = 0x200000003 # 表示该节点的子节点列表最后一次修改的事务ID,添加子节点或删除子节点会影响子节点列表,
# 但是修改子节点的数据内容则不影响该ID(注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid)
cversion = 1 # 子节点的版本
dataVersion = 0 # 数据版本号,数据每次修改该版本号加1
aclVersion = 0 # 权限版本号,权限每次修改该版本号加1
ephemeralOwner = 0x0 # 创建该临时节点的会话的sessionID。(如果该节点是持久节点,那么这个属性值为0)
dataLength = 9 # 该节点的数据长度
numChildren = 1 # 该节点拥有子节点的数量(只统计直接子节点的数量)
# 创建临时节点 create -e
$ create -e /aha/temp temp-value
# 创建顺序节点 create -s 就是会在目录的后面添加一串序列号
$ create /aha/aha
Created /aha/aha0000000003
# 创建临时顺序节点的话就是
$ create -e -s
# 修改节点的值 用 set 命令
$ set path data [-v version]
- version: 是可选项,版本号,可以作为乐观锁,只有设置的时候,version字段填写正确才能正确的设置值
# 当 version 不正确的时候是设置不成功的
$ set /LOCK/aha uuu -v 3
# 同步命令 后面加的是目录
sync /aha
可以在客户端中直接使用 help
查看命令的使用方法
临时节点(目录)下面是不能有子节点的
搭建 zk 集群
节点规划
这边使用 master01,node01,node02
三台机器进行集群的搭建演示,地址分别为 10.211.255.3,10.211.55.4,10.211.55.5
。
安装的目录为:/home/parallels/Downloads/zookeeper
。
第一步仍然是安装 JDK
以及安装包的下载,zk
环境变量的配置,参考单节点章节,不在赘述。
这边设置一下主机的名称,这样不容易在操作多台主机的时候分不清哪一台是哪一台:
$ hostnamectl set-hostname master01
$ hostnamectl set-hostname node01
$ hostnamectl set-hostname node02
设置完成之后,重启机器,重新连接,就可以看到:
FAQ
:为了防止脑裂的问题,zk
集群节点的个数要求是2n + 1
。
修改 zoo.cfg
的配置
# 在 conf 下,拷贝出一个 zoo.cfg
$ cp zoo_sample.cfg zoo.cfg
zoo.cfg
文件的简单说明:
dataDir=/tmp/zookeeper
:zk
数据保存的位置,默认在 tmp
下,这个文件夹是存放临时数据的位置,建议修改;
zk
三个端口的说明:
2181
: 对client
端提供服务2888
: 集群内机器通信使用3888
: 选举leader
使用
tickTime
:心跳间隔(毫秒),超过2倍tickTime
将会被认为会话超时
initLimit=10
: 初始化连接时最长的时间(心跳间隔次数),超过该值会认为连接超时
syncLimit=5
:zk
集群中leader
和follower
同步消息的超时时间(心跳间隔次数)
clientPort
: 服务器监听的端口号,用于客户端连接服务器
修改 zoo.cfg
文件:
# 1. 在三台机器的 zoo.cfg 文件末尾添加
server.1=10.211.55.3:2888:3888
server.2=10.211.55.4:2888:3888
server.3=10.211.55.5:2888:3888
# 2. 修改 zoo.cfg 的 dataDir
dataDir=/home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin/data
拓展阅读:如果这边需要
observer
的话,可以添加这一行配置server.4=10.211.55.6:2888:3888:observer
添加 myid
在 dataDir
配置的文件夹中添加 myid
文件,文件的内容便是 zoo.cfg
中 server.n
的 n
。例如:10.211.55.3
中就应该填写 1
。
依次操作其他几台节点。
启动集群
# 1. 启动集群的时候三台机器依次执行 - 注意这边是三台机器都要手动启动的
$ zkServer.sh start
# 2. 查看集群的状态,在节点上执行
$ zkServer.sh status
# follower 节点显示如下内容
root@master01:/home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin/conf# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
# leader 节点显示如下内容
root@node01:/home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin/conf# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /home/parallels/Downloads/zookeeper/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
踩坑说明:
当你
telnet
一个端口不通的时候,有一下两种情况:
防火墙的问题: 遇到这种情况可以关闭防火墙或者增加防火墙的端口
你访问的那台机器压根没有监听那个端口: 排查这种问题应该在被访问的机器上执行如下吗命令:
netstat -lntup
可以结合grep
命令使用
使用客户端测试集群
# 注意这边连接的时候要使用 2181 端口
$ zkCli.sh -server 10.211.55.4:2181
zk 图形界面工具
https://github.com/zzhang5/zooinspector
SpringBoot
集成 zk
使用 zkClient
操作 zk
添加 maven
依赖
<!-- 创建 Zookeeper 客户端依赖,一定要和 Zookeeper Server 版本保持一致 curator依赖中其实也包含,但是这边为了版本
对应显示的声明了一下-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
<exclusions>
<!--因为 zk 包使用的是 log4j 日志,和 springboot 的logback 日志冲突 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
编写配置
zookeeper:
# zookeeper Server 地址,如果有多个使用逗号分隔。如 ip1:port1,ip2:port2,ip3:port3
address: 10.211.55.3:2181,10.211.55.4:2181,10.211.55.5:2181
retryCount: 5 # 重试次数
initElapsedTimeMs: 1000 # 初始重试间隔时间
maxElapsedTimeMs: 5000 # 最大重试间隔时间
sessionTimeoutMs: 30000 # Session 超时时间
connectionTimeoutMs: 10000 # 连接超时时间
编写配置类
package com.aha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 连接 zookeeper 配置类
*
* @author: WT
* @date: 2021/11/22 18:14
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "zookeeper")
public class ZkClientProperties {
/** 重试次数 */
private int retryCount;
/** 初始重试间隔时间 */
private int initElapsedTimeMs;
/** 最大重试间隔时间 */
private int maxElapsedTimeMs;
/**连接地址 */
private String address;
/**Session过期时间 */
private int sessionTimeoutMs;
/**连接超时时间 */
private int connectionTimeoutMs;
}
注册 zkClient
package com.aha.client;
import com.aha.config.ZkClientProperties;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 生成 zk 客户端
*
* @author: WT
* @date: 2021/11/22 18:18
*/
@Configuration
public class ZookeeperClient {
@Bean
private static ZkClient zkClient (ZkClientProperties zookeeperProperties) {
// 这边如果需要扩展参数的话,可以看 ZkClient 的构造函数
return new ZkClient(new ZkConnection(zookeeperProperties.getAddress()), zookeeperProperties.getConnectionTimeoutMs());
}
}
演示 zkClient
的监听事件
编写 service
演示类
package com.aha.lock.service;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.stereotype.Service;
/**
* 测试 zkClient 的监听事件
*
* @author: WT
* @date: 2021/11/23 14:53
*/
@Slf4j
@Service
public class ZkWatcher {
private final ZkClient zkClient;
private static final String NODE_PATH = "/path";
private static final String LOG_SEPARATOR = "++++++++++++++++++++++++++++++++++++++++++++";
public ZkWatcher (ZkClient zkClient) {
this.zkClient = zkClient;
}
public void testSubscribeChildChanges () throws InterruptedException {
/*
* 匿名内部类重写是 IZkChildListener
* 监听指定节点和指定节点下子节点的变化
* parentPath:我们指定要监听的节点
* currentChildren:指定要监听的节点下的所有子节点
* 当指定节点和指定节点下子节点发生 增删 操作时会被监听到,更新节点子节点和父节点的内容是不会被监听的, 递归删除删除几次会触发几次
* 这边可以看到日志 Delivering event #1 done
*/
zkClient.subscribeChildChanges(NODE_PATH, (parentPath, currentChildren) -> {
log.info("当前节点为: {}, 当前节点下的子节点为:{}", parentPath, currentChildren);
log.info(LOG_SEPARATOR);
});
operateNode(zkClient);
}
public void testSubscribeDataChanges () throws InterruptedException {
zkClient.subscribeDataChanges(NODE_PATH, new IZkDataListener() {
/**
* 监听节点的 删除操作
* @param path 节点路径
* @throws Exception 异常
*/
@Override
public void handleDataDeleted(String path) throws Exception {
log.info("删除的节点为: {}", path);
log.info(LOG_SEPARATOR);
}
/**
* 可以监听节点的 创建 更新 操作
* @param path 节点路径
* @param data 变更的内容
* @throws Exception 异常
*/
@Override
public void handleDataChange(String path, Object data) throws Exception {
log.info("变更的节点为:{}, 变更内容为: {}", path , data);
log.info(LOG_SEPARATOR);
}
});
operateNode(zkClient);
}
public static void operateNode (ZkClient zkClient) throws InterruptedException {
// 递归删除
zkClient.deleteRecursive(NODE_PATH);
// 节点已经存在的话是会抛异常的 : ZkNodeExistsException
zkClient.createPersistent(NODE_PATH);
Thread.sleep(1000);
// SubscribeChildChanges 不会监听父节点的 update 操作; SubscribeDataChanges 会监听节点的 update 操作
zkClient.writeData(NODE_PATH,"父节点发生变化");
Thread.sleep(1000);
zkClient.createPersistent(NODE_PATH + "/" + "c1", "c1内容");
Thread.sleep(1000);
zkClient.createPersistent(NODE_PATH + "/" + "c2", "c2内容");
Thread.sleep(1000);
//不会监听子节点的 update 操作
zkClient.writeData(NODE_PATH + "/" + "c1","c1新内容");
Thread.sleep(1000);
zkClient.delete(NODE_PATH + "/c2");
Thread.sleep(1000);
// 使用此方法删除的节点如果不为空的话会抛异常 ZkException
// zkClient.delete(NODE_PATH);
// 递归删除: 这里发生了两次,一次是先删除/super/c1触发一次,然后再删除/super再触发一次
zkClient.deleteRecursive(NODE_PATH);
// 这个延时是必须的,因为如果你的 main 方法直接停止的话,监听程序也就关闭了,监听是异步的,后面的操作可能还没监听到线程就停止了这个不合理的
Thread.sleep(10000);
log.info("延时结束");
}
}
编写测试 controller
package com.aha.lock.controller;
import com.aha.lock.service.InterprocessMutexLock;
import com.aha.lock.service.ZkWatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: WT
* @date: 2021/11/23 14:13
*/
@RestController
public class TestController {
private final InterprocessMutexLock interprocessMutexLock;
private final ZkWatcher zkWatcher;
public TestController (
InterprocessMutexLock interprocessMutexLock,
ZkWatcher zkWatcher
) {
this.interprocessMutexLock = interprocessMutexLock;
this.zkWatcher = zkWatcher;
}
/**
* 测试 zkClient 的 SubscribeDataChanges 方法
*/
@GetMapping("/zk-client/subscribe/data")
public void testZkClientSubscribeDataChanges () throws InterruptedException {
zkWatcher.testSubscribeDataChanges();
}
/**
* 测试 zkClient 的 SubscribeChildChanges 方法
*/
@GetMapping("/zk-client/subscribe/child")
public void testZkClientSubscribeChildChanges () throws InterruptedException {
zkWatcher.testSubscribeChildChanges();
}
}
使用 Curator
操作 zk
添加 maven
依赖
<!-- 创建 Zookeeper 客户端依赖,一定要和 Zookeeper Server 版本保持一致 curator依赖中其实也包含,但是这边为了版本
对应显示的声明了一下-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
<exclusions>
<!--因为 zk 包使用的是 log4j 日志,和 springboot 的logback 日志冲突 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes
他包含了分布式锁的实现-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
编写配置
zookeeper:
# zookeeper Server 地址,如果有多个使用逗号分隔。如 ip1:port1,ip2:port2,ip3:port3
address: 10.211.55.3:2181,10.211.55.4:2181,10.211.55.5:2181
retryCount: 5 # 重试次数
initElapsedTimeMs: 1000 # 初始重试间隔时间
maxElapsedTimeMs: 5000 # 最大重试间隔时间
sessionTimeoutMs: 30000 # Session 超时时间
connectionTimeoutMs: 10000 # 连接超时时间
编写配置类
package com.aha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 连接 zookeeper 配置类
*
* @author: WT
* @date: 2021/11/22 18:14
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "zookeeper")
public class ZkClientProperties {
/** 重试次数 */
private int retryCount;
/** 初始重试间隔时间 */
private int initElapsedTimeMs;
/** 最大重试间隔时间 */
private int maxElapsedTimeMs;
/**连接地址 */
private String address;
/**Session过期时间 */
private int sessionTimeoutMs;
/**连接超时时间 */
private int connectionTimeoutMs;
}
注册 CuratorFramework
package com.aha.client;
import com.aha.config.ZkClientProperties;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 生成 zk 客户端
*
* @author: WT
* @date: 2021/11/22 18:18
*/
@Configuration
public class ZookeeperClient {
/**
* initMethod = "start"
* curatorFramework 创建对象之后,调用 curatorFramework 实例的 start 方法
*/
// @Bean(initMethod = "start")
// public CuratorFramework curatorFramework(ZkClientProperties zookeeperProperties) {
// return CuratorFrameworkFactory.newClient(
// zookeeperProperties.getAddress(),
// zookeeperProperties.getSessionTimeoutMs(),
// zookeeperProperties.getConnectionTimeoutMs(),
// new RetryNTimes(zookeeperProperties.getRetryCount(), zookeeperProperties.getInitElapsedTimeMs())
// );
// }
@Bean(initMethod = "start")
private static CuratorFramework getZkClient(ZkClientProperties zookeeperProperties) {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, zookeeperProperties.getRetryCount(), 5000);
return CuratorFrameworkFactory.builder()
.connectString(zookeeperProperties.getAddress())
.sessionTimeoutMs(zookeeperProperties.getSessionTimeoutMs())
.connectionTimeoutMs(zookeeperProperties.getConnectionTimeoutMs())
.retryPolicy(retryPolicy)
.build();
}
}
演示 curator
的提供的分布式可重入排它锁
编写 service
演示类
package com.aha.lock.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.zookeeper.data.Stat;
import org.springframework.stereotype.Service;
/**
* 测试 curator 提供的 分布式可重入排它锁
*
* @author: WT
* @date: 2021/11/22 17:41
*/
@Slf4j
@Service
public class InterprocessMutexLock {
private final CuratorFramework curatorFramework;
private int inventory = 40;
public InterprocessMutexLock (CuratorFramework curatorFramework) {
this.curatorFramework = curatorFramework;
}
public void test(String lockPath) {
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
//模拟 50 个线程抢锁
for (int i = 0; i < 50; i++) {
new Thread(new TestThread(i, lock)).start();
}
}
class TestThread implements Runnable {
private final Integer threadFlag;
private final InterProcessMutex lock;
public TestThread(Integer threadFlag, InterProcessMutex lock) {
this.threadFlag = threadFlag;
this.lock = lock;
}
@Override
public void run() {
try {
lock.acquire();
log.info("第 {} 个线程获取到了锁, 库存还剩余:{}", threadFlag, inventory);
if (inventory > 0) {
inventory --;
log.info("库存还剩余:{}", inventory);
} else {
log.info("已经没有库存了 inventory: {}, 线程 {} 没有买到", inventory, threadFlag);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
编写测试 controller
package com.aha.lock.controller;
import com.aha.lock.service.InterprocessMutexLock;
import com.aha.lock.service.ZkWatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: WT
* @date: 2021/11/23 14:13
*/
@RestController
public class TestController {
private final InterprocessMutexLock interprocessMutexLock;
private final ZkWatcher zkWatcher;
public TestController (
InterprocessMutexLock interprocessMutexLock,
ZkWatcher zkWatcher
) {
this.interprocessMutexLock = interprocessMutexLock;
this.zkWatcher = zkWatcher;
}
/**
* 测试 curator 提供的 分布式可重入排它锁
*/
@GetMapping("/lock/mutex")
public void testMutexLock () {
interprocessMutexLock.test("/lock/mutex");
}
}
关于代码的示例可以参考:
https://github.com/WT-AHA/JAVA-TREASURE.git
下的lock
下的distributed-lock-zookeeper
工程
FAQ
zk 的不足
zk
的性能有限,tps
大概只能达到一万多,因为只有leader
节点可以处理事务请求。zk
选取主节点的时候是没有办法对外服务的,而且这个过程也相对较慢。zk
的权限控制相对较简单。zk
进行读取的时候可能读取的是旧数据。因为zk
的过半原则,当有一半以上的机器数据同步完成就认为是数据同步完成了,你读取的那个节点可能就是没有同步的那个节点。
zk 是 cp 还是 ap
关于 zk
是 cp
还是 ap
从不同的角度分析应该是不一致的:
zk
在leader
选举期间,会暂停对外提供服务(zk
依赖leader
来保证数据一致性),所以丢失了可用性,保证了一致性,即cp
。但这边的数据一致性指的又是应该是属于最终一致性不是强一致性,当过半的节点数据同步完成,集群就认为数据同步完成了。- 一个读请求过来,为了保证可用性,不用阻塞到所有的
follower
同步完成,就可以提供数据服务,这样看又是ap
。
所以 zk
是使用 最终一致性的顺序一致性 和 过半机制 平衡了 cap
理论,但是如果是选择题的话的应该选择 cp
好一点。
扩展阅读:在分析 cap
的时候,p
一般是必须要满足的,要不就有悖于分布式的原则,所以一般是在 c 和 a
之间进行取舍。
常见组件 cap
分类:
eureka
属于ap
etcd
属于cp
consul
属于cp