Zookeeper 源码解读系列补漏之加载快照数据到内存

前言

在讲解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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值