前言
在讲解Zookeeper服务端启动的时候,发现漏掉了一个知识点。就是在启动服务端的时候,服务端会从快照中把数据先load进入内存中去,但是这部分被遗漏了,重新写一篇帖子补上,会安插在遗漏的地方。既然已经提出来了,那么就作为一个章节点,好好的讲解一下。
介绍以前还是要讲一下打快照的逻辑,打快照是SyncRequestProcessor这个处理器类做的,我们在处理器链的章节里已经分析过了。它首先先对事务进行持久化,然后再打快照。但是这里有一个细节,并不是每一个事务来了都会打一个快照,会累积随机数量的事务然后打一个快照,最大是1000,但是这个随机数字我们可以自己设置,这是一个前提,我们后面的例子会用。本篇也会被收录到【Zookeeper 源码解读系列目录】中。
加载数据的流程
无论是集群模式还是单机模式,在服务端启动的时候都会走到zkDb.loadDataBase()这个方法里去,顾名思义就是加载数据,那么我们就跳进去看:
public long loadDataBase() throws IOException {
long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
initialized = true;
return zxid;
}
这里snapLog
就是工具类FileTxnSnapLog
,所以要看restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener)
这个方法,我们这里传入的dataTree
就是我们打的快照的内容,接着进入:
public long restore(DataTree dt, Map<Long, Integer> sessions, PlayBackListener listener) throws IOException {
snapLog.deserialize(dt, sessions);//把数据加载到DataTree中
return fastForwardFromEdits(dt, sessions, listener);//找事务并取出
}
这里面的snapLog
还是快照的日志,然后snapLog.deserialize(dt, sessions);
把快照的日志反序列化出来数据,加载到DataTree
中,deserialize
里就是具体反序列化的内容,那么后面return的是个什么东西呢?return
的这个方法fastForwardFromEdits(dt, sessions, listener);
就是找事务并取出用的。那么我们就走进去看看这个快照是怎么取出来的:
public long fastForwardFromEdits(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
FileTxnLog txnLog = new FileTxnLog(dataDir);//找到事务的文件
TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
long highestZxid = dt.lastProcessedZxid;//取出快照里面最大的Zxid
TxnHeader hdr;
try {
while (true) {//循环事务id
hdr = itr.getHeader();//新构建一个事务header
if (hdr == null) {
return dt.lastProcessedZxid;//说明取出的数据和快照一致
}
if (hdr.getZxid() < highestZxid && highestZxid != 0) {
LOG.error("{}(higestZxid) > {}(next log) for type {}",
new Object[] { highestZxid, hdr.getZxid(),
hdr.getType() });
} else {
highestZxid = hdr.getZxid();//修改highestZxid
}
try {
//把没有执行的事务再次执行并更新内存返回给客户端
processTransaction(hdr,dt,sessions, itr.getTxn());
} catch(KeeperException.NoNodeException e) {
throw new IOException("Failed to process transaction type: " +
hdr.getType() + " error: " + e.getMessage(), e);
}
listener.onTxnLoaded(hdr, itr.getTxn());
if (!itr.next())//循环下一个事务
break;
}
} finally {
if (itr != null) {
itr.close();
}
}
return highestZxid;
}
首先是要找到事务文件txnLog = new FileTxnLog(dataDir);
,然后构建事物的迭代器TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
取出全部的事务id
,并做成一个iterator
,用来还原已经执行的事务。我们看一下参数dt.lastProcessedZxid
,这个参数是快照中记录的最大的事务id
,这里为什么会加1呢?因为我们的日志里数据是不会消失的,加1是为了检测:是否有些没有打到快照里,但是应该被执行的日志。如果有的话,就会走到下面processTransaction()
重新执行并更新到DataTree
里。这里比较不好理解,我在最后会做一个例子讲解。接着取出highestZxid = dt.lastProcessedZxid;
快照里面最大的Zxid
。
这样就走到while(true)
里面了,在这里循环事务id
。先给当前的事务新构建一个事务头hdr = itr.getHeader();
,如果if (hdr == null)
成立,说明取出的数据和快照一致,直接返回Zxid
就可以了。这里其实也可以看出来,这个while循环就是在把事务文件里的事务和快照里的事务进行比较,为了更好区分两者,下面就用事务文件和快照作为替代了,接着走。
下一个if
语句if (hdr.getZxid() < highestZxid && highestZxid != 0)
如果说事务文件id
小于快照id
,就报错。因为打快照是在事务写文件之后执行的,所以理论上不会存在这样的情况,一般都会走到else
中,就把快照的id
更新为事务文件的id
。更新之后调用processTransaction(hdr,dt,sessions, itr.getTxn());
把没有执行的事务再此执行并更新内存返回给客户端,再提醒一下打快照在更新内存之前,有了快照内存未必更新,所如果发现快照和事务文件不一样,必须发回更新内存。
最后if (!itr.next())
继续下一个事务的流程。那么接着我们就一个例子重新讲解一遍代码,大家就更加清楚这里面是怎么做的了。
异常处理
我们做一个假设我现在有8个事务已经做了持久化,zxid是1 - 8,如下:
1
2
3
4 -------打快照的位置-------
5
6
7
8 -------日志持久化的位置-------
假设服务端刚刚在4
的位置打了快照,注意事务已经写到了8
,这个时候服务器挂了。那么下次服务器重启的时候,如果只是从快照里面导出数据(1-4)
到内存,那么后面这些事务(5-8)
是不是根本无法生效了。
那么我们的快照有个最大的事务id:4
,那么取事务文件的时候就把快照里面最大的id+1
取出来,这里就是刚才取事务文件txnLog.read(dt.lastProcessedZxid+1)
中的id
要用快照的最大id+1
的原因。那么我们取出来的是什么呢,就是5对吧。
1
2
3
4 -------打快照的位置------- zxid=4
5 -------取出来的位置------- zxid=5
6
7
8 -------日志持久化的位置-------zxid=8
如果说5
找的是null
,说明事务文件里面也没有这个事务,那就直接返回当前的id。但是如果说找到5
了不是null
,那么就掉用processTransaction(hdr,dt,sessions, itr.getTxn());
把这个事务更新到内存DataTree
里面去。就是这样一条一条的把6、7、8的全部更新进去。
下面是这个例子按照程序执行的步骤,应该更加能够帮助大家理解:
public long fastForwardFromEdits(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
FileTxnLog txnLog = new FileTxnLog(dataDir);//找到事务的文件
//取出全部的事务id,并做成一个iterator,用来还原已经执行的事务,
// dt.lastProcessedZxid这个是快照中记录的最大的事务id,
// 我们一共有1-8个日志,快照到4中断了(lastProcessedZxid=4)
// 本来我们取出的itr应该是4-8,但是经过传入txnLog.read(lastProcessedZxid+1)的操作以后,
// 这里我们取出来的itr里面的数据应该是5-8的数据。
TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
long highestZxid = dt.lastProcessedZxid;//这里取出快照里面最大的Zxid
TxnHeader hdr;
try {
while (true) {//循环事务,那么第一次进来必定是事务5
//新构建一个事务header。通过上面的分析,如果事务5有内容,则hdr必定不是null,
// 如果快照和持久化最大都是4,则hdr必定什么内容都没有是null
hdr = itr.getHeader();
if (hdr == null) {
//hdr是null说明取出的数据和快照一致,都是4,直接返回Zxid
return dt.lastProcessedZxid;
}
//按照我们的例子这里的判断就是 5<4 && true = false, 那么我们就到else里
if (hdr.getZxid() < highestZxid && highestZxid != 0) {
LOG.error("{}(higestZxid) > {}(next log) for type {}",
new Object[] { highestZxid, hdr.getZxid(),
hdr.getType() });
} else {
highestZxid = hdr.getZxid();//把最大的highestZxid修改为5
}
try {
//把没有执行的事务再此执行并更新内存返回给客户端
processTransaction(hdr,dt,sessions, itr.getTxn());
} catch(KeeperException.NoNodeException e) {
throw new IOException("Failed to process transaction type: " +
hdr.getType() + " error: " + e.getMessage(), e);
}
listener.onTxnLoaded(hdr, itr.getTxn());
//这里itr.next(),把自身的zxid更新为6,再循环一遍到7,8,
// 达成条件break,跳出循环完成所有事务的更新
if (!itr.next())
break;
}
} finally {
if (itr != null) {
itr.close();
}
}
return highestZxid;
}