0%

LevelDB源码解析(六)- WAL和Log文件

前言

数据库中基本都有WAL功能,LevelDB也不例外,不过这里的WAL功能实现比较简单

Log文件初始化

在DbImpl的构造函数中,申请了一个新的FileNumber,然后对log对象进行了初始化。

1
2
long logFileNumber = versions.getNextFileNumber();
this.log = Logs.createLogWriter(new File(databaseDir, Filename.logFileName(logFileNumber)), logFileNumber);

所以这个Log文件每次启动都会创建一个新的。

写入

Log的写入也比较简单,每次进行put的时候,将KV序列化特定的格式,然后append进日志系统。

1
2
3
4
5
6
7
public Snapshot writeInternal(WriteBatchImpl updates, WriteOptions options) {
//...
// Log write
Slice record = writeWriteBatch(updates, sequenceBegin);
log.addRecord(record, options.sync());
//...
}

更迭

在正常的运行中,Log日志会更迭吗?

因为如果不更迭的话,日志会越来越多,单文件可能会越来越多。

同时我们知道Log格式的文件仅仅支持从头开始遍历的,在Recover的时候,从头到尾遍历一个大文件也是个问题。

makeRoomForWrite中,如果MemTable需要进行Compact的时候,就会强制关闭当前的Log,再创建一个新的Log。

1
2
3
4
5
6
7
private void makeRoomForWrite(boolean force) {
// ...
log.close();
long logNumber = versions.getNextFileNumber();
this.log = Logs.createLogWriter(new File(databaseDir, Filename.logFileName(logNumber)), logNumber);
// ...
}

注意:makeRoomForWrite并不是每次调用都会走到这个逻辑的。

删除

什么时候删除呢?

前面我们看到每次启动时,会新生成一个新的Log,每次CompactMemTable的时候,也会新生成一个新的,那么旧的什么时候删除呢?

答案在deleteObsoleteFiles方法中。

方法内容如名字,其实这个方法不止删除了旧的Log文件。

这个方法会遍历数据目录下的所有文件,如果是log文件,判断fileNumber是否小于当前的LogFileNumber,如果小于,就可以删除。

了解了内容,我们来想想这个方法会在什么时候调用呢?

前面提到过,每次Compact新的MemTable的时候,都会生成新的Log文件,也就是说,这个Log文件包含了当前MemTable中的内容,也就是未持久化的内容。

如果Compact后,持久化了,自然就不需要这个文件了。

所以在VersionSet::logAndApply后,都会清理旧的Log文件。

因为每次VersionEdit生成后,NewVersion的所有SSTable就已经确定了,新的KV记录则在新的Log日志中,也就不需要再知道旧的Log日志文件是啥了。

Recover

结合了WAL的性质,我们来了解下,recover的内容。

按照理解,Recover时,主要把有内容还是MemTable中,还没持久化到磁盘上,这个时候需要找到这个MemTable对于的Log文件,遍历这个文件内容,再Append一遍就行。

我们结合DbImpl的构造函数一起来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public DbImpl(Options options, File databaseDir) {
// ...
versions = new VersionSet(databaseDir, tableCache, internalKeyComparator);
// load (and recover) current version
versions.recover(); // 1
long minLogNumber = versions.getLogNumber();
long previousLogNumber = versions.getPrevLogNumber();

List<File> filenames = Filename.listFiles(databaseDir);
List<Long> logs = new ArrayList<>();
for (File filename : filenames) {
FileInfo fileInfo = Filename.parseFileName(filename);
if (fileInfo != null && fileInfo.getFileType() == FileType.LOG && ((fileInfo.getFileNumber() >= minLogNumber) || (fileInfo.getFileNumber() == previousLogNumber))) {
logs.add(fileInfo.getFileNumber()); // 2
}
}
// Recover in the order in which the logs were generated
VersionEdit edit = new VersionEdit();
Collections.sort(logs);
for (Long fileNumber : logs) {
long maxSequence = recoverLogFile(fileNumber, edit); // 3
if (versions.getLastSequence() < maxSequence) {
versions.setLastSequence(maxSequence);
}
}
// open transaction log
long logFileNumber = versions.getNextFileNumber();
this.log = Logs.createLogWriter(new File(databaseDir, Filename.logFileName(logFileNumber)), logFileNumber);
edit.setLogNumber(log.getFileNumber());
// apply recovered edits
versions.logAndApply(edit); // 4
// cleanup unused files
deleteObsoleteFiles(); // 5
}
  1. 读取CURRENT文件执行的Manifest文件,恢复出最后的LogNumber。
  2. 这里我不理解为什么Logs是个数组,讲道理每次MemTable都是串行Compact的话,那么Manifest中最后一个VersionEdit中的LogNumber,就是最新的MemTable的内容,不会出现多个文件的情况。
  3. 这里的recoverLogFile方法,就是顺序遍历,然后Put进去,但是这里和正常的Put流程不一样,虽然会生成新的SSTable,但是并不会调用VersionSet的logAndApply方法。
  4. 这里对Edit进行了LogAndApply,提交到了Manifest中,数据恢复成功
  5. 可以清理旧的文件了