Zookeeper简介、数据结构与监听机制
Zookeeper简介
Zookeeper是什么?
Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。分布式系统中数据存在一致性的问题!!
- ZooKeeper 本质上是一个分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
- ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理(solr的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。
zookeeper的架构组成
Leader
- Zookeeper 集群工作的核心角色
- 集群内部各个服务器的调度者。
- 事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;对于 create,setData, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。
Follower
- 处理客户端非事务(读操作) 请求,
- 转发事务请求给 Leader;
- 参与集群 Leader 选举投票 2n-1台可以做集群投票。
此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色。
Observer
- 观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
- 不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求。
ZK也是Master/slave架构,但是与之前不同的是zk集群中的Leader不是指定而来,而是通过选举产生。
Zookeeper特点
- Zookeeper:一个领导者(leader:老大),多个跟随者(follower:小弟)组成的集群。
- Leader负责进行投票的发起和决议,更新系统状态(内部原理)
- Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
- 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。(因为投票的结果可以超过半数,即可以在剩下节点中选出Leader继续工作)
- 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一致的。(Leader写的数据都会同步给所有的follower,也就相当于是所有的follower保存的都是 Leader数据的副本)
- 更新请求顺序进行(内部原理)
- 数据更新原子性,一次数据更新要么成功,要么失败。
Zookeeper数据结构与监听机制
ZooKeeper数据模型Znode
在ZooKeeper中,数据信息被保存在一个个数据节点上,这些节点被称为znode。ZNode 是Zookeeper 中最小数据单位,在 ZNode 下面又可以再挂 ZNode,这样一层层下去就形成了一个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采用了类似文件系统的层级树状结构进行管理。见下图示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37LWikxq-1617846140009)(en-resource://database/667:0)]
在 Zookeeper中,每一个数据节点都是一个 ZNode,上图根目录下有两个节点,分别是:app1 和 app2,其中 app1 下面又有三个子节点,所有ZNode按层次化进行组织,形成这么一颗树,ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点写入数据,也可以在这个节点下面创建子节点。
ZNode的类型
刚刚已经了解到,Zookeeper的znode tree是由一系列数据节点组成的,那接下来,我们就对数据节点做详细讲解.
Zookeeper 节点类型可以分为三大类:
- 持久性节点(Persistent)
- 临时性节点(Ephemeral)
- 顺序性节点(Sequential)
在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。不同类型的节点则会有不同的生命周期:
**持久节点:**是Zookeeper中最常见的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除
**持久顺序节点:**就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序。
**临时节点:**就是会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。
**临时顺序节点:**就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。
事务ID
首先,先了解,事务是对物理和抽象的应用状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,一般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
而在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用 ZXID 来表示,通常是一个 64 位的数字。每一个 ZXID 对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序
zk中的事务指的是对zk服务器状态改变的操作(create,update data,更新字节点);zk对这些事务操作都会编号,这个编号是自增长的被称为ZXID。(ZK可以利用该ZXID实现分布式锁机制)
ZNode的状态信息
整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。数据内容是空,其他的属于状态信息。这些节点的状态信息分别的含义如下所示:
cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后一次被修改的时间。
pZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子
节点内容变更不会更新。
cversion 表示子节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据长度。
numChildren 表示直系子节点数。
Watcher 机制
Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能
一个典型的发布/订阅模型系统定义了一种 一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
在 ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么Zk就会向指定客户端发送一个事件通知来实现分布式的通知功能。
整个Watcher注册与通知过程如图所示。
- Client向ZK注册监听器。监听某个目录 可以监听下面的子节点, 也可以监听下面的数据。
- 客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。用来对各种监听器进行管理。
- 当Zookeeper服务器触发Watcher事件后,会向客户端发送通知。如果监听的目录中的数据或节点发生了改变,ZK就会发送一个通知到Client。这里的流程是把Watcher对象发送到WatchManager里,则之前存储的watcher对象里面的内容就会被更新。更新之后,Client就会从WatchManager中再次获取到watcher对象,然后调用接收到通知之后的执行逻辑,比如是要把变化后的监听数据拿回来还是去做其他事情
Watcher注册监听器实例:基于Zookeeper实现简易版配置中心
需求:
- 创建一个Web项目,将数据库连接信息交给Zookeeper配置中心管理,即:当项目Web项目启动时,从Zookeeper进行MySQL配置参数的拉取
- 要求项目通过数据库连接池访问MySQL(连接池可以自由选择熟悉的)
- 当Zookeeper配置信息变化后Web项目自动感知,正确释放之前连接池,创建新的连接池
第一步:创建web工程,客户端连接ZK集群
public class ZkServlet extends HttpServlet{
ZkClient zkClient = null;
Connection conn = null;
DataSource dataSource = null;
/*
* init: servlet对象创建的,调用此方法完成初始化操作
* */
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// 客户端连接ZK集群
new ZkClient("linux121:2181, linux122:2181, linux123:2181");
// 判断节点是否存在,不存在创建节点并赋值
boolean exists = zkClient.exists("/mysql_configuration");
if (!exists)
{
zkClient.createEphemeral("/mysql_configuration", "{'driverClassName':'com.mysql.jdbc.Driver', 'url':'jdbc:mysql://linux123:3306/zookeeper?characterEncoding=UTF-8', 'username':'root', 'password':'123'}");
}
// 设置自定义的序列化类型
zkClient.setZkSerializer(new ZkStrSerializer());
}
这里将Mysql的配置信息组织成Map的格式存储在ZK结群的某一节点上,方便后续客户端的读取和使用。
第二步:从ZK集群上拉取Mysql配置文件信息,并注册监听器
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 初始化zk的监听器
registerListener();
// 客户端从节点上读取数据
String data = (String) zkClient.readData("/mysql_configuration");
// 利用JSON将读取下的数据格式转为Map类型,方便后续创建数据库连接池对象
Map<String, String> map = JSON.parseObject(data, new TypeReference<Map<String, String>>(){});
System.out.println("MysqlConfiguration: " + map);
try {
// 根据拉取的Mysql配置信息获取数据库连接
conn = getDruidConnection(map);
// 根据连接池查询数据库数据
queryMysqlData(conn);
// 向前端返回Mysql的配置信息
resp.getWriter().write(map.toString());
// 休眠10s后向zookeeper中写入新的配置文件
Thread.sleep(10000);
// 休眠10s后,更新该节点的数据,观察监听器的功能
zkClient.writeData("/mysql_configuration", "{'driverClassName':'com.mysql.jdbc.Driver', 'url':'jdbc:mysql://linux123:3306/zookeeper?characterEncoding=UTF-8', 'username':'root', 'password':'12345678'}");
// 测试结束,关闭资源
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
图中各自定义函数如下所示:
// 注册监听器
public void registerListener(final HttpServletResponse resp)
{
// 注册监听器,节点数据改变的类型,接收通知后的处理逻辑定义
zkClient.subscribeDataChanges("/mysql_configuration", new IZkDataListener() {
// path: 是监听的数据路径
// data: 改变之后的新的数据
public void handleDataChange(String path, Object data) throws Exception {
// 定义接收通知之后的处理逻辑
// 首先释放原先的连接池
conn.close();
// 根据新的配置信息,创建新的连接池
// 获取新的配置信息
Map<String, String> newMysqlConf = getMysqlConf((String) data);
System.out.println("New Mysql Configuration: " + newMysqlConf);
// 创建新的连接池连接数据
conn = getDruidConnection(newMysqlConf);
// 根据新连接查询数据
queryMysqlData(conn);
resp.getWriter().write("New Mysql Configuration: " + newMysqlConf);
}
// 处理数据的删除 -> 节点删除
public void handleDataDeleted(String path) throws Exception {
System.out.println(path + " is deleted!!");
}
});
}
public Map<String, String> getMysqlConf(String data)
{
// 将字符串的data数据转换为Map类型
Map<String, String> map = JSON.parseObject((String) data, new TypeReference<Map<String, String>>(){});
return map;
}
// 通过连接池获取jdbc连接对象
public Connection getDruidConnection(Map map) throws Exception {
dataSource = DruidDataSourceFactory.createDataSource(map);
conn = dataSource.getConnection();
return conn;
}
// 查询表中的数据并打印
public void queryMysqlData(Connection conn) throws SQLException {
Statement statement = conn.createStatement();
String sql = "select * from homework";
ResultSet resultSet = statement.executeQuery(sql);
while(resultSet.next())
{
System.out.println(resultSet.getInt("id"));
System.out.println(resultSet.getString("name"));
System.out.println(resultSet.getString("addr"));
}
}
利用subscribeDataChanges
函数来实现对节点数据变化的监听。这里要注意的就是handleDataChange(String path, Object data)
方法里的data参数是改变之后的新的数据内容。因此通过代码的实际使用也可以重新理解最开始介绍的Watcher机制的过程。如果监听的目录中的数据或节点发生了改变,ZK就会发送一个通知到Client。就是把Watcher对象发送到WatchManager里,则之前存储的watcher对象里面的内容就会被更新。而Client从WatchManager中再次获取到watcher对象,此时的watcher对象的内容就是被更新后的数据信息(也就是参数data数据)。