Zookeeper是一个分布式的协调服务系统,提出了一个在分布式环境下一致性问题的解决方案。这里简单介绍下Zookeeper的几种基础知识。
数据模型
数据模型是一种存储和处理数据的逻辑结构。 Zookeeper中的数据模型是树形结构,类似电脑的文件系统。我们可以在根节点下创建子节点,并在子节点下继续创建下一级节点。Zookeeper树种的每一层级用斜杠(/)分隔,并且只能用绝对路径查询Zookeeper的节点。
节点类型
Zookeeper中的数据节点也分为持久节点、临时节点和有序节点。
1、持久节点
持久节点,即节点一旦作为持久节点创建,该数据节点会一直存储在Zookeeper服务器上。即使创建该节点的客户端与服务端的会话关闭,节点依旧不会被删除。
2、临时节点
临时节点,不会一直存储在Zookeeper服务器上。当创建该临时节点的客户端会话关闭时,该节点也相应在Zookeeper服务器上被删除。
我们通常而言使用临时节点来做服务器集群内机器运行情况的统计。比如将集群设置为"/servers"节点,并为集群下每台服务器创建一个临时节点。当服务器下线时该节点被删除。最后我们通过统计临时节点的个数即可知道集群的运行情况。
3、有序节点
有序节点,是在持久节点和临时节点的基础上,增加了节点有序的特性。所谓节点有序,是指Zookeeper会自动使用一个单调递增的数字作为该节点的后缀。
Zookeeper中的每个节点都维护有一个存储节点数据、ACL访问控制信息、节点数据的二进制数组,和一个记录自身状态信息的字段stat。
节点状态结构
在客户端执行 stat /节点名 可以看到控制台输出的节点状态信息。
ZooKeeper中卫数据节点引入了版本的概念。每个数据节点有三种版本:对节点数据内容、子节点信息或者是ACL信息的修改次数。
使用Zookeeper实现锁
情景:如何解决发生超卖的问题? 这里主要介绍两种锁:悲观锁、乐观锁。
悲观锁
悲观锁认为,进程对临界区的资源的竞争会一直出现,因此该条数据会一直处于被锁定状态。 对于悲观锁,我们通常使用临时节点,避免因进程异常中断而导致锁一直存在,并通过服务器端添加监听事件来获取其他进程重新获取锁。
乐观锁
乐观锁,在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。 乐观锁,可以分为读取、检验、写入三个步骤。如CAS就是一个乐观锁的实现。 在Zookeeper中,version属性是用于实现乐观锁的校验的。 在 ZooKeeper 的底层实现中,当服务端处理 setDataRequest 请求时,首先会调用 checkAndIncVersion 方法进行数据版本校验。Zookeeper从中获取当前版本的version,同时通过 getRecordForPath 方法获取服务器数据记录 nodeRecord, 从中得到当前服务器上的版本信息 currentversion。若version为-1,则不适用乐观锁。若不为-1,则对比version和currentversion。
关于乐观锁和悲观锁,可参见:juejin.cn/post/694028…
Zookeeper只能使用绝对路径,而不能使用相对路径,是由于其底层使用hashtableConcurrentHashMap来实现的。
Watch机制
我们可以使用Watch监控机制来实现一个发布订阅的功能。 实现发布订阅功能,有几个核心节点:用户端注册服务、服务端处理请求、客户端收到回调后执行相应操作。 使用Watch机制,可以通过向客户端的构造方法传递Watch参数 或者getData()方法
Watch机制的底层原理
其结构类似“观察者模式”,一个对象或者数据节点可能会被多个客户端监控,当对应事件被触发时,会通知这些对象或客户端。
ZooKeeper 中在客户端和服务器端分别实现两个存放观察者列表,即:ZKWatchManager 和 WatchManager。
Watch机制如何实现
客户端Watch注册实现过程
客户端的 Watcher 机制是一次性的,触发后就会被删除。 发送一个 Watch 监控事件的会话请求时,ZooKeeper 客户端主要做了两个工作:
- 标记该会话是一个带有 Watch 事件的请求
- 将 Watch 事件存储到 ZKWatchManager
服务端Watch注册实现过程
Zookeeper 服务端处理 Watch 事件基本有 2 个过程:
- 解析收到的请求是否带有 Watch 注册事件
- 将对应的 Watch 事件存储到 WatchManager
服务端 Watch 事件的触发过程
在客户端和服务端都对 watch 注册完成后,即可触发Watch事件。 在对节点数据内容进行变更后,会调用WatchManager.triggerWatch 方法触发数据变更事件。 triggerWatch内,一共做了以下几件事情:
- 封装具有会话状态、事件类型、数据节点3种属性的WatchedEvent对象
- 查询该节点注册的Watch事件。若存在Watch事件,则添加到定义的Watchers集合中,并在WatchManager管理中删除
- 调用process方法向客户端发送通知。
客户端回调的处理过程
使用SendThread.readResponse() 方法来统一处理服务端的相应。
- 反序列化服务器发送请求头信息,判断相属性字段 xid 的值为 -1,表示该请求响应为通知类型。
- 将己收到的字节流反序列化转换成 WatcherEvent 对象
- 判断客户端是否配置了 chrootPath 属性,若为true,对接收到的节点路径进行 chrootPath 处理。
- EventThread.queueEvent( )方法将接收到的事件交给 EventThread 线程进行处理
EventThread.queueEvent() 方法内部的执行逻辑:
- 从 ZKWatchManager 中查询注册过的客户端 Watch 信息,并将其从ZKWatchManager的管理中删除。
- 将查询到的 Watcher 存储到 waitingEvents 队列,调用 EventThread 类中的 run 方法,环取出在 waitingEvents 队列中等待的 Watcher 事件进行处理。
- 调用processEvent(event) 方法执行实现了 Watcher 接口的 process()方法。
Watch 具有一次性,所以当我们获得服务器通知后要再次添加 Watch 事件。