Zookeeper--数据模型、事务ID、结点类型和版本

数据模型

ZooKeeper的视图结构和标准的Unx文件系统非常类似,但没有引入传统文件系统中目录和文件等相关概念,而是使用了其特有的“数据节点”概念,我们称之为ZNode。 ZNode是ZooKeeper中数据的最小单元,每个 ZNode上都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,我们称之为树。

在ZooKeeper中,每一个数据节点都被称为-个ZNode,所有ZNode按层次化结构进行组织,形成- - 棵树。ZNode 的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/) 进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。
在这里插入图片描述

事务ID

在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。

对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID来表示,通常是一个64位的数字。每一一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。

结点类型

在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型。在ZooKeeper中,节点类型可以分为持久节点(PERSISTENT)、临时节点( EPHEMERAL)和顺序节点(SEQUENTIAL) 三大类,具体在节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:

  • 持久节点(PERSISTENT)
    持久节点是ZooKeeper中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。

  • 持久顺序节点(PERSISTENT_SEQUENTIAL)
    持久顺序节点的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。

  • 临时节点(EPHEMERAL)
    和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外,ZooKeeper规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。

  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)
    临时顺序节点的基本特性和临时节点也是一致的,同样是在临时节点的基础上,添加了顺序的特性。

状态信息:

事实上,每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。使用get命令来获取一个数据节点的内容:
在这里插入图片描述
在这里插入图片描述

版本(保证分布式数据原子性操作)

ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。
在这里插入图片描述
ZooKeeper中的版本概念和传统意义上的软件版本有很大的区别,它表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数,我们以其中的version 这种版本类型为例来说明。在一个数据节点/zk-book被创建完毕之后,节点的version 值是0,表示的含义是“当前节点自从创建之后,被更新过0次”。如果现在对该节点的数据内容进行更新操作,那么随后,version的值就会变成1。同时需要注意的是,在上文中提到的关于version的说明,其表示的是对数据节点数据内容的变更次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值依然会变更。

一个多线程应用,尤其是分布式系统,在运行过程中往往需要保证数据访问的排他性。例如在最常见的车站售票系统上,在对系统中车票“剩余量”的更新处理中,我们希望在针对某个时间点的数据进行更新操作时(这可能是一个极短的时间间隔,例如几秒或几毫秒,甚至是几纳秒,在计算机科学的有些应用场景中,几纳秒可能也算不上太短的时间间隔),数据不会因为其他人或系统的操作再次发生变化。也就是说,车站的售票员在卖票的过程中,必须要保证在自己的操作过程中,其他售票员不会同时也在出售这个车次的车票。

为保证上面这个场景的正常运作,一种可能的做法或许是这样,车站某售票窗口的售票员突然向其他售票员大喊一声:“现在你们不要出售杭州到北京的XXX次车票!”然后当他售票完毕后,再次通知大家:“该车次已经可以售票啦!”

当然在现实生活中,不会依靠这么原始的人工方式来实现数据访问的排他性,但这个例子给我们的启发是:在并发环境中,我们需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,我们称这样的机制为“锁”。在数据库技术中,通常提到的“悲观锁”和“乐观锁”就是这种机制的典型实现。

悲观锁,又被称作悲观并发控制(Pessimistic Concurrency Control, PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同–数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权力更新这份数据。一般我们认为,在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景一在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制问题。

乐观锁,又被称作乐观并发控制(Optimistic Concurrency Control, OCC), 也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。从上面对悲观锁的讲解中我们可以看到,悲观锁假定不同事务之间的处理一定会出现互相干扰,从而需要在一个事务从头到尾的过程中都对数据进行加锁处理。而乐观锁则正好相反,它假定多个事务在处理过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理。当然,既然有并发,就一定会存在数据更新冲突的可能。在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。乐观锁通常适合使用在数据并发竞争不大、事务冲突较少的应用场景中。

事实上,在ZooKeeper中,version属性正是用来实现乐观锁机制中的“写入校验”的。在ZooKeeper服务器的PrepRequestProcessor处理器类中,在处理每一个 数据更新(setDataRequest) 请求时,会进行如下所示的版本检查。

//setData版本检查
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if (version != -1 && version != currentVersion) {
	throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;

从上面的执行逻辑中,我们可以看出,在进行一次setDataRequest请求处理时,首先进行了版本检查:ZooKeeper会从setDataRequest请求中获取到当前请求的版本version, 同时从数据记录nodeRecord中获取到当前服务器上该数据的最新版本currentVersion:

  • "如果version为“-1”, 那么说明客户端并不要求使用乐观锁,可以忽略版本比对;
  • 如果version不是“-1”, 那么就比对version和currentVersion,如果两个版本不匹配,那么将会抛出BadVersionException异常。
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值