ZooKeeper数据模型
ZooKeeper的视图结构和标准的Unix文件系统非常类似,使用了其特有的"数据节点"概念。(不是真的文件系统,注意区别)
我们称之为"ZNode",ZNode是zookeeper中数据的最小单元,每个ZNode上都可以保存数据,同时还可以挂载子节点,我们称之为"树"。
这里不再多说"树"这个概念..
事务ID
数据库事务具有所谓的ACID特性:原子性(Atomic),一致性(Consistency),隔离性(Isolation),持久性(Durability)
在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作。
《从PAXOS到ZOOKEEPER分布式一致性原理与实践》书中对于事务的说明:
数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。
《ZooKeeper分布式过程技术详解》书中对于事务的说明:
那些会改变ZooKeeper状态的客户端请求[create(创建)、delete(删除)、setData(更新)],将会被转发给群首(leader),群首执行相应得到请求,并形成状态的更新,我们称之为"事务"
两本书中对事务说明,最大的区别是"客户端会话的创建于失效"
我个人认为事务只是"创建、删除、更新"
对于客户端的连接与失效也是事务,保留态度╮(╯▽╰)╭。
对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID来表示,通常是一个64位的数字。
每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。
节点特性
节点类型
ZooKeeper中,每个数据节点都有生命周期,其生命周期的长短取决于数据节点的节点类型。
在ZooKeeper中,节点类型可以分为持久节点、临时节点和顺序节点三大类。
持久节点(PERSISTENT):
指该数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。
持久顺序节点(PERSISTENT_SEQUENTIAL):
与持久节点的特性一致,额外的特性表现在顺序性上。
在ZooKeeper中,每个父节点都会为它的第一级自接到点维护一份顺序(序列),用于记录下每个子节点创建的先后顺序。
临时节点(EPHEMERAL):
临时节点的生命周期与客户端的会话是绑定在一起的。
也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。
临时顺序节点(EPHEMERAL_SEQUENTIAL):
临时顺序节点的基本特性和临时节点一致,同样是在临时节点的基础上,增加了顺序的特性。
状态信息
每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。
如下图所示:
这些就是节点数据的Stat对象的格式化输出
我们来看下参数的含义:
czxid | 即CreateZXID,表示数据节点被创建时的事务ID |
mzxid | 即ModifiedZXID,表示节点最后一次被更新时的事务ID |
ctime | 即CreatedTime,表示节点被创建的时间 |
mtime | 即ModifiedTime,表示节点最后一次被更新的时间 |
version | 数据节点的版本号 |
cversion | 子节点的版本号 |
aversion | 节点的ACL版本号 |
ephemeralOwner | 创建该临时节点的会话的SessionID。 如果是永久节点,那么这个属性值为0 |
datalength | 数据内容的长度 |
numChildren | 当前节点的子节点个数 |
pzxid
| 表示节点的子节点列表最后一次被修改时的事务ID。 注意:只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid |
版本——保证分布式数据原子性操作
ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息。
对数据节点的任何更新操作都会引起版本号的变化。
version | 数据节点的版本号 |
cversion | 子节点的版本号 |
aversion | 节点的ACL版本号 |
ZooKeeper中的版本概念和传统意义上的软件版本有很大的区别,它表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数。
我们用version举个栗子:
数据节点"/zk"被创建之后,节点的version值是0,表示的含义是"当前节点自从创建之后,被更新过0次"。
如果现在对该节点的数据内容进行更新操作,那么随后,version的值就会变成1。
同时需要注意:
version表示的是对数据节点的数据内容变更的次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值依然会变更。
好,现在问题来了..
这个版本究竟是干嘛用的呢?
我们先来看下分布式领域中最常见的一个概念——锁!
(原书讲的比较细,有兴趣的童鞋可以去看原书,这里我挑重点)
悲观锁
悲观锁,又被称为悲观并发控制,是数据库中一种非常典型且非常严格的并发控制策略。
悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。
简单的说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权利更新这份数据。
乐观锁
乐观锁,又被称作乐观并发控制器,也是一种常见的并发控制策略,相比悲观锁,乐观锁机制显得更加宽松与友好。
在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。
如果其他事务有更新的话,那么正在提交的事务就需要回滚。
通过上面的了解,我们可以把乐观锁控制的事务分成如下三个阶段:数据读取、写入校验、数据写入。
其中写入校验阶段是整个乐观锁控制的关键所在。
在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。
在JDK中乐观锁的实现是"CAS"——对于V值,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化的更新到新值B,其中是否符合预期便是乐观锁中的"写入校验"阶段。
上面讲了那么多,现在我们来看看ZooKeeper中版本的作用。
事实上,在ZooKeeper中,version属性正是用来实现乐观锁机制的"写入校验"的。
我们看下ZooKeeper服务端PrepRequestProcessor处理器类的源码:(原书中的)
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if (version != -1 && version != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;
从上面执行的逻辑可以看出:
在进行一次setDataRequest请求处理时
a. 首先进行版本检查:ZooKeeper会从setDataRequest请求中获取到当前请求的版本Version,
b. 同时从数据记录nodeRecord中获取到当前服务器上该数据最新版本currentVersion。
如果version为"-1",那么说明客户端并不要求使用乐观锁,可以忽略版本比对。
如果version不是"-1",那么就比对version 和 currentVersion,如果两个版本不匹配,那么就抛出异常。
(我看的有些迷糊..回头再捋...暂时放着...)
Watcher——数据变更的通知
原书讲的比较细,我挑通知状态看下..感兴趣的童鞋,看原书..
Watcher事件
(直接贴原书中的图片)