ZKDatabase类维护着zookeeper的内存数据库,具体包括了数据树,会话信息和事务提交日志等。
首先,我们不妨来看一下ZKDatabase类的静态变量和成员变量。
//为该类创建日志记录器
private static final Logger LOG = LoggerFactory.getLogger(ZKDatabase.class);
//要维护的四个成员变量
protected DataTree dataTree;
protected ConcurrentHashMap sessionsWithTimeouts;
protected FileTxnSnapLog snapLog;
protected long minCommittedLog, maxCommittedLog;
dataTree:维护了树形数据结构,不包含任何网络相关或客户端连接相关的代码。
sessionWithTimeouts:存储sessionID和对应的过期时间,使用并发哈希表ConcurrentHashMap作为变量类型,这是HashMap的一个线程安全的、支持高效并发的版本。
(PS:这里简单介绍一下sessionID和对应的过期时间的含义。
sessionID: 会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,zookeeper 都会为其分配一个全局唯一的 sessionID。
Timeout:会话超时时间。客户端在构造 Zookeeper 实例时候,向服务端发送配置的超时时间,server 端会根据自己的超时时间限制最终确认会话的超时时间。)
snapLog:维护事务日志和数据快照,snapLog和具体的zookeeper数据库是一一对应的。
minCommittedLog,maxCommittedLog:zookeeper维护commitedLog队列,minCommittedLog和maxCommitedLog分别对应队列中事务编号的最小值和最大值(值得注意的是,zookeeper指定了这个队列有个默认最大长度500如下图,当队列更新要超过最大长度时,会移除目前队列中最早的事务)。
public static final String COMMIT_LOG_COUNT = "zookeeper.commitLogCount";
public static final int DEFAULT_COMMIT_LOG_COUNT = 500;
public int commitLogCount;
PS:zookeeper的事务ID(zxid)遵循如下规则:
zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
DataTree-zookeeper数据库的核心上面简单介绍了ZKDatabase类后,让我们先把焦点转到DataTree类。(我们先对ZKDatabase类中一个实例所要维护的这几个变量的结构做深入了解,之后再回到ZKDatabase类,看它提供了哪些函数哪些行为来维护这些变量)。DataTree无疑是zookeeper所构建的内存数据库的核心部分,其维护了一个树形数据结构。
DataTree主要维护两个结构,一个是各个绝对路径到数据节点的哈希表,另一个是由数据节点构成的树。并且对路径的所有访问是通过哈希表,仅在序列化到磁盘时遍历该树。如下成员变量nodes即存储着哈希表,是对哈希表的简单封装。
//日志
private static final Logger LOG = LoggerFactory.getLogger(DataTree.class);
private final RateLogger RATE_LOGGER = new RateLogger(LOG, 15 * 60 * 1000);
private final NodeHashMap nodes;
//watch的管理(zookeeper通过watch对客户端提供了通知节点变化的机制)
private IWatchManager dataWatches;
private IWatchManager childWatches;
//存储所有节点的路径及数据占的空间大小
private final AtomicLong nodeDataSize = new AtomicLong(0);
//zookeeper中树的根节点为"/"
private static final String rootZookeeper = "/";
DataTree构造函数
public DataTree() {
this(new DigestCalculator());
}
DataTree(DigestCalculator digestCalculator) {
this.digestCalculator = digestCalculator;
nodes = new NodeHashMapImpl(digestCalculator);
//添加根节点
nodes.put("", root);
nodes.putWithoutDigest(rootZookeeper, root);
//分别添加配额节点和管理配额的节点
root.addChild(procChildZookeeper);
nodes.put(procZookeeper, procDataNode);
procDataNode.addChild(quotaChildZookeeper);
nodes.put(quotaZookeeper, quotaDataNode);
addConfigNode();
nodeDataSize.set(approximateDataSize());
try {
//配置监听器
dataWatches = WatchManagerFactory.createWatchManager();
childWatches = WatchManagerFactory.createWatchManager();
} catch (Exception e) {
LOG.error("Unexpected exception when creating WatchManager, exiting abnormally", e);
ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
}
}
PS:关于配额,是指节点可以设置配额,如限制节点可以有几个子节点。但事实上,如果超过了配额限制,也只是在日志上会出现警告信息,实际内容还是被存储了。
这里把构造函数写成一个public,一个default,而public实际上基本只是调用default构造函数的形式我觉得可能仅仅是为了方便测试,事实上,在系统实现代码上都是调的public的构造函数,只有在测试代码中用到了default构造函数。
题外趣事在阅读源码的过程中,我发现即使是出自apache这样代码质量比较高的组织,代码中也会出现p,pp...这样的命名方式。



