文章目录
一、概念介绍
1. 简介
Zookeeper是一个开源的分布式协调服务,由雅虎公司开发,用于解决分布式应用中的一致性问题。它提供了一个分布式的协调服务,可以实现分布式应用的元数据管理、分布式锁、分布式队列、节点选举、集群管理等功能。
2. 数据模型
ZooKeeper 提供的名称空间与标准文件系统的名称空间非常相似。名称是由斜杠 (/) 分隔的一系列路径元素。ZooKeeper 命名空间中的每个节点都由路径标识。
上图中每一个节点可以称之为znode,每个znode都有如下属性:
- data:znode存储的业务数据,注意父节点不能存储数据
- children:存储当前节点的字节点的引用信息,因为内存限制,所以 znode 的子节点数不是无限的
- stat: 包含 znode 节点的状态信息,比如: 事务 id、版本号、时间戳等,其中事务 id 和 ZK 的数据一直性、选主相关
- acl: 记录客户端对 znode 节点的访问权限
znode 的数据操作具有原子性,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。znode 可存储的最大数据量是 1MB ,但实际上我们在 znode 的数据量应该尽可能小,因为数据过大会导致 zk 的性能明显下降。每个 ZNode 都对应一个唯一的路径。
znode节点根据生命周期的不同可以划分为持久节点和临时节点。持久节点的存活时间不依赖于客户端会话,只有客户端在显式执行删除节点操作时,节点才消失;临时节点的存活时间依赖于客户端会话,当会话结束,临时节点将会被自动删除。(当然也可以手动删除临时节点)注意:临时节点不能拥有子节点。通过create /zk_test1 data1就创建了一个数据为data1的持久节点zk_test1,通过create -e /zk_test2 data2就创建了一个数据为data2的临时节点zk_test2。同时,create命令还可以加 -s 参数指定节点是否有序,创建顺序节点时,zk 会在路径后面自动追加一个递增的序列号 ,这个序列号可以保证在同一个父节点下是唯一的,具体有如下四种节点:
- PERSISTENT:永久节点
- EPHEMERAL:临时节点
- PERSISTENT_SEQUENTIAL:永久顺序节点
- EPHEMERAL_SEQUENTIAL:临时顺序节点
3. watcher监听机制
Zookeeper的Watcher是一种事件监听机制,用于监视Zookeeper上节点的状态变化。当某个节点发生变化时,Zookeeper会向客户端发送通知,从而触发客户端的Watcher回调函数。
具体实现步骤如下:
- 客户端调用Zookeeper的API注册Watcher,并指定回调函数。
- Zookeeper服务端在节点状态发生变化时,将通知发送给客户端。
- 客户端接收到通知后,会调用事先注册的回调函数进行处理。
- 如果客户端需要继续监控该节点状态的变化,就需要重新注册Watcher。
Watcher具有以下特点:
- 是一种轻量级的机制,不需要客户端主动轮询节点的状态,能够有效减少网络通信量和客户端的负载。
- 是一种一次性的机制,即一旦客户端收到了Watcher的通知,该Watcher就会被删除,需要重新注册才能再次接收通知。
- 是一种异步机制,当节点状态变化时,Zookeeper会异步发送通知给客户端,客户端通过回调函数处理通知。
- 可以应用于节点的数据变化、子节点的变化和节点的删除事件,能够满足各种监控和协调需求。
4. 如何保证数据一致性和可靠性
数据复制机制
Zookeeper采用了一种高度可靠的数据复制机制,即ZAB(Zookeeper Atomic Broadcast)协议,用于在多个服务器之间同步数据。在ZAB协议中,每个服务器都可以扮演Leader和Follower两个角色。Leader负责处理客户端请求,Follower负责从Leader处同步数据,并保持数据的一致性。当Leader失效时,Follower会发起选举,选出新的Leader来处理客户端请求。ZAB协议保证了数据的一致性和可靠性,即使有多个服务器失效,也能保证数据的安全。
严格的数据版本控制
Zookeeper采用了严格的数据版本控制机制,每个节点都有一个版本号,每次更新节点数据时,版本号都会自增。客户端在修改节点数据时,需要指定版本号,只有当版本号与当前节点的版本号匹配时,才能成功修改节点数据。这种版本控制机制可以防止多个客户端同时修改同一个节点数据的问题,保证了数据的一致性和可靠性。
5. zookeeper如何实现分布式锁
Zookeeper实现分布式锁主要基于其节点操作和Watcher机制来实现。具体过程如下:
- 客户端在Zookeeper中创建一个临时有序节点(例如/lock/lock-0001),同时在节点上注册一个Watcher。
- 客户端调用getChildren()方法获取同一父节点下所有的子节点,判断当前客户端创建的节点是否为所有子节点中序号最小的节点。如果是,说明当前客户端获取到了锁,可以执行相应的操作;如果不是,说明当前客户端没有获取到锁,需要等待。
- 如果当前客户端没有获取到锁,则可以调用exists()方法并注册一个Watcher来监听它前一个节点的状态变化,当前一个节点被删除时,客户端可以重新执行第2步。
- 当客户端执行完操作后,需要删除创建的临时节点,释放锁。
上述过程中,通过创建临时有序节点,Zookeeper保证了节点的唯一性和顺序性,通过Watcher机制可以实现节点状态的监听和回调。这种分布式锁机制能够有效避免分布式环境下的资源竞争问题,保证了分布式应用的数据一致性和可靠性。
二、安装部署
1. 单机
安装包下载地址,这里选择最新的稳定版本3.7.1,然后下载后缀为bin.tar.gz的压缩包。
上传到服务器某位置,解压tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz
,进入conf目录,将zoo_simple.cfg文件重命名为zoo.cfg,mv zoo_sample.cfg zoo.cfg
,高版本的zookeeper默认加载conf/zoo.cfg,修改配置文件如下
#ZooKeeper 使用的基本时间单位,以毫秒为单位。它用于执行心跳,最小会话超时将是 tickTime 的两倍
tickTime=2000
#initLimit和syncLimit是针对集群的参数,分别为通信时限和同步时限
initLimit=10
syncLimit=5
#存储内存数据库快照的位置,除非另有说明,否则存储更新数据库的事务日志
dataDir=/usr/local/zookeeper
#客户端连接的端口
clientPort=2181
执行bin/zkServer.sh start
命令启动zookeeper服务
执行bin/zkCli.sh -server 127.0.0.1:2181
连接到zookeeper服务端
可以根据官方文档查看命令,比如:
version
查看版本信息
create /test_node test_data
创建节点,且存储数据为字符串test_data
get /test_node
获取节点数据
2. 集群
将上述单机步骤的配置文件修改为zoo-1.cfg,mv zoo.cfg zoo-1.cfg
,修改配置文件
#ZooKeeper 使用的基本时间单位,以毫秒为单位。它用于执行心跳,最小会话超时将是 tickTime 的两倍
tickTime=2000
#initLimit和syncLimit是针对集群的参数,分别为通信时限和同步时限
initLimit=10
syncLimit=5
#存储内存数据库快照的位置,除非另有说明,否则存储更新数据库的事务日志
dataDir=/usr/local/zookeeper1
#客户端连接的端口
clientPort=2181
#server.<节点ID>=<IP>:<数据同步端口>:<选举端口>
#<节点ID>:代表第几号服务器
#<IP>:这个服务器的IP
#<数据同步端口>:这个服务器与集群中的Leader服务器交换信息的端口
#<选举端口>:如果Leader挂了,需要一个新的端口进行选举,选出的新Leader的端口
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
接下来我们复制zoo-1.cfg,复制出2和3,修改clientPort和dataDir即可。然后,在前面创建的三个存储目录编辑myid文件,分别输入1,2,3,接下来通过使用不同配置文件启动
bin/zkServer.sh start conf/zoo-1.cfg
bin/zkServer.sh start conf/zoo-2.cfg
bin/zkServer.sh start conf/zoo-3.cfg
通过查看各个节点状态,可以发现1和3是从节点,2是主节点
bin/zkServer.sh status conf/zoo-1.cfg
bin/zkServer.sh status conf/zoo-2.cfg
bin/zkServer.sh status conf/zoo-3.cfg
那么接下来说一说leader的选举机制
第一次启动: 目前有1、2、3三台机器,依次启动
- 1号启动给自己投了一票,这时没有超过半数以上,因此,没有leader,1号机器处于looking状态
- 2号机器启动,也给自己一票,此时1号机器发现2号机器的myid大于自己,因此将自己的票投给2号,此时超过一半以上,因此2号机器为leader,1号为follower
- 3号机器启动后,此时1号机器已经没了选票,2号有两张,3号自己一张,少数服从多数,3号也成为follower。
非第一次启动:
- leader存活,只是某个follower挂掉,当它重新启动之后,集群中还是存在leader的,只需要重新建立连接
- leader不存活,每个节点在运行时都有如下的三个id(epoch,zxid,sid),epoch是leader任期时id(选举一次为一期);zxid是事务id,用于标识每个节点的事务操作序列;sid就是其myid。当某时刻leader宕机后,首先看存活节点的epoch id,最大的选举为leader,如果epoch都相同,则看其事务zxid ,选举zxid最大的为leader,如果zxid都相同,则看其sid,就是节点的myid,myid大的为leader
三、springboot集成
1. 引入依赖
在Zookeeper官网提供了Java Api的示例,但是原生的Api需要开发人员创建对象、注册watcher等,因此这里使用一个封装好的curator,它提供了各种应用场景,实现了Fluent风格的Api接口,是操作zk的最好的框架。
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
2. 配置
server.port=8002
server.servlet.context-path=/zk
#zk地址
curator.connectString=192.168.1.1:2181,192.168.1.1:2182,192.168.1.1:2183
#重试次数
curator.retryCount=1
#重试时间间隔
curator.elapsedTimeMs=2000
#session超时时间
curator.sessionTimeoutMs=6000
#连接超时时间
curator.connectionTimeoutMs=10000
3. 测试
配置类
package com.example.zookeeper.zookeeper;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZKConfig {
@Value("${curator.connectString}")
private String connectString;
@Value("${curator.retryCount}")
private String retryCount;
@Value("${curator.elapsedTimeMs}")
private String elapsedTimeMs;
@Value("${curator.sessionTimeoutMs}")
private String sessionTimeoutMs;
@Value("${curator.connectionTimeoutMs}")
private String connectionTimeoutMs;
@Bean("CuratorFramework")
public CuratorFramework curatorFramework() throws InterruptedException {
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(
connectString,
Integer.valueOf(sessionTimeoutMs),
Integer.valueOf(connectionTimeoutMs),
new RetryNTimes(Integer.valueOf(retryCount),Integer.valueOf(elapsedTimeMs))
);
curatorFramework.start();
curatorFramework.blockUntilConnected();
return curatorFramework;
}
}
测试类
package com.example.zookeeper.zookeeper;
import org.apache.curator.framework.CuratorFramework;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
@RestController
public class TestController {
@Autowired
private CuratorFramework client;
@RequestMapping("/test")
public void test(){
System.out.println(client.toString());
}
@RequestMapping("/query")
public String query(@RequestParam String url) throws Exception{
byte[] data = client.getData().forPath(url);
return new String(data);
}
@RequestMapping("/put")
public String put(@RequestParam String url,String value) throws Exception{
String path = client.create().creatingParentsIfNeeded().forPath(url, value.getBytes(StandardCharsets.UTF_8));
return path;
}
}