Zookeeper学习(三)----Zookeeper的读写流程
ZooKeeper的读写流程分析
在了解了ZooKeeper的基本概念后,我们来详细看下ZooKeeper的读写流程,以及ZooKeeper在并发情况下的读写控制。以求对ZooKeeper有进一步的了解。
读流程分析
读流程如下图所示:
image因为ZooKeeper集群中所有的server节点都拥有相同的数据,所以读的时候可以在任意一台server节点上,客户端连接到集群中某一节点,读请求,然后直接返回。当然因为ZooKeeper协议的原因(一半以上的server节点都成功写入了数据,这次写请求便算是成功),读数据的时候可能会读到数据不是最新的server节点,所以比较推荐使用watch机制,在数据改变时,及时感应到。
写流程分析
写流程如下图所示:
image当一个客户端进行写数据请求时,会指定ZooKeeper集群中的一个server节点,如果该节点为Follower,则该节点会把写请求转发给Leader,Leader通过内部的协议进行原子广播,直到一半以上的server节点都成功写入了数据,这次写请求便算是成功,然后Leader便会通知相应Follower节点写请求成功,该节点向client返回写入成功响应。
ZooKeeper并发读写情况分析
我们已经知道ZooKeeper的数据模型是层次型,类似文件系统,不过ZooKeeper的设计目标定位是简单、高可靠、高吞吐、低延迟的内存型存储系统,因此它的value不像文件系统那样适合保存大的值,官方建议保存的value大小要小于1M,key为路径。
image每个数据节点在ZooKeeper中叫做znode,并且其有一个唯一的路径标识,znode节点可以包含数据和子节点。
ZooKeeper的层次模型是通过ConcurrentHashMap实现的,key为path,value为DataNode,DataNode保存了znode中的value、children、 stat等信息。
ZooKeeper的层次模型是通过ConcurrentHashMap实现的,而ConcurrentHashMap是线程安全的Hash Table,它采用了锁分段技术来减少锁竞争,提高性能的同时又保证了并发安全,其结构如下图所示:
imageConcurrentHashMap由两部分组成,Segment和HashEntry,锁的粒度是Segment,每个Segment 对象包含整个散列映射表的若干个桶,散列冲突时通过链表来解决。
因为插入键 / 值对操作只是在 Segment 包含的某个桶中完成,所以这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,不需要锁定整个ConcurrentHashMap,所以对于ConcurrentHashMap,可以进行并发的写操作,只要写入的Segment不同。而所有的读线程几乎不会因为写操作的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值。这便是锁分段技术,保证并发安全的情况下又提高了性能。
对于ZooKeeper来讲,ZooKeeper的写请求由Leader处理,Leader能够保证并发写入的有序性,即同一时刻,只有一个写操作被批准,然后对该写操作进行全局编号,最后进行原子广播写入,所以ZooKeeper的并发写请求是顺序处理的,而底层又是用了ConcurrentHashMap,理所当然写请求是线程安全的。而对于并发读请求,同理,因为用了ConcurrentHashMap,当然也是线程安全的了。总结来说,ZooKeeper的并发读写是线程安全的。
但是对于ZooKeeper的客户端来讲,如果使用了watch机制,在进行了读请求但是watch znode前这段时间中,如果znode的数据变化了,客户端是无法感知到的,这段时间客户端的数据就有一定的滞后性了,只有当下次数据变化后,客户端才能感知到,所以对于客户端来说,数据是最终一致性。