leveldb 源码分析 19¶
本系列《leveldb 源码分析》共有 22 篇文章,这是第十九篇
11.VersionSet 分析之 2¶
11.4 LogAndApply()¶
函数声明:
Status LogAndApply(VersionEdit*edit, port::Mutex* mu)
前面接口小节中讲过其功能:在 currentversion 上应用指定的 VersionEdit,生成新的 MANIFEST 信息,保存到磁盘上,并用作 current version,故为 Log And Apply。 参数 edit 也会被函数修改。
11.4.1 函数流程¶
下面就来具体分析函数代码。 S1 为 edit 设置 log number 等 4 个计数器。
if (edit->has_log_number_) {
assert(edit->log_number_ >= log_number_);
assert(edit->log_number_ < next_file_number_);
}
else edit->SetLogNumber(log_number_);
if (!edit->has_prev_log_number_) edit->SetPrevLogNumber(prev_log_number_);
edit->SetNextFile(next_file_number_);
edit->SetLastSequence(last_sequence_);
要保证 edit 自己的 log number 是比较大的那个,否则就是致命错误。保证 edit 的 log number 小于 next file number,否则就是致命错误 - 见 9.1 小节。
S2 创建一个新的 Version v,并把新的 edit 变动保存到 v 中。
Version* v = new Version(this);
{
Builder builder(this, current_);
builder.Apply(edit);
builder.SaveTo(v);
}
Finalize(v); //如前分析,只是为v计算执行compaction的最佳level
S3 如果 MANIFEST 文件指针不存在,就创建并初始化一个新的 MANIFEST 文件。这只会发生在第一次打开数据库时。这个 MANIFEST 文件保存了 current version 的快照。
std::string new_manifest_file;
Status s;
if (descriptor_log_ == NULL) {
// 这里不需要unlock *mu因为我们只会在第一次调用LogAndApply时
// 才走到这里(打开数据库时).
assert(descriptor_file_ == NULL); // 文件指针和log::Writer都应该是NULL
new_manifest_file = DescriptorFileName(dbname_, manifest_file_number_);
edit->SetNextFile(next_file_number_);
s = env_->NewWritableFile(new_manifest_file, &descriptor_file_);
if (s.ok()) {
descriptor_log_ = new log::Writer(descriptor_file_);
s = WriteSnapshot(descriptor_log_); // 写入快照
}
}
S4 向 MANIFEST 写入一条新的 log,记录 current version 的信息。在文件写操作时 unlock 锁,写入完成后,再重新 lock,以防止浪费在长时间的 IO 操作上。
[cpp] view plain copy
mu->Unlock();
if (s.ok()) {
std::string record;
edit->EncodeTo(&record);// 序列化current version信息
s = descriptor_log_->AddRecord(record); // append到MANIFEST log中
if (s.ok()) s = descriptor_file_->Sync();
if (!s.ok()) {
Log(options_->info_log, "MANIFEST write: %s\n", s.ToString().c_str());
if (ManifestContains(record)) { // 返回出错,其实确实写成功了
Log(options_->info_log, "MANIFEST contains log record despiteerror ");
s = Status::OK();
}
}
}
//如果刚才创建了一个MANIFEST文件,通过写一个指向它的CURRENT文件
//安装它;不需要再次检查MANIFEST是否出错,因为如果出错后面会删除它
if (s.ok() && !new_manifest_file.empty()) {
s = SetCurrentFile(env_, dbname_, manifest_file_number_);
}
mu->Lock();
S5 安装这个新的 version
if (s.ok()) { // 安装这个version
AppendVersion(v);
log_number_ = edit->log_number_;
prev_log_number_ = edit->prev_log_number_;
}
else { // 失败了,删除
delete v;
if (!new_manifest_file.empty()) {
delete descriptor_log_;
delete descriptor_file_;
descriptor_log_ = descriptor_file_ = NULL;
env_->DeleteFile(new_manifest_file);
}
}
流程的 S4 中,函数会检查 MANIFEST 文件是否已经有了这条 record,那么什么时候会有呢?
主函数使用到了几个新的辅助函数 WriteSnapshot,ManifestContains 和 SetCurrentFile,下面就来分析。
11.4.2 WriteSnapshot()¶
函数声明:
Status WriteSnapshot(log::Writer*log)
把 currentversion 保存到 *log 中,信息包括 comparator 名字、compaction 点和各级 sstable 文件,函数逻辑很直观。
- S1 首先声明一个新的 VersionEdit edit;
- S2 设置 comparator:edit.SetComparatorName(icmp_.user_comparator()->Name());
- S3 遍历所有 level,根据 compact_pointer_[level],设置 compaction 点: edit.SetCompactPointer(level, key);
- S4 遍历所有 level,根据 current_->files_,设置 sstable 文件集合:edit.AddFile(level, xxx)
- S5 根据序列化并 append 到 log(MANIFEST 文件)中;
std::string record;
edit.EncodeTo(&record);
returnlog->AddRecord(record);
以上就是 WriteSnapshot 的代码逻辑。
11.4.3 ManifestContains()¶
函数声明:
bool ManifestContains(conststd::string& record)
如果当前 MANIFEST 包含指定的 record 就返回 true,来看看函数逻辑。
- S1 根据当前的 manifest_file_number_ 文件编号打开文件,创建 SequentialFile 对象
- S2 根据创建的 SequentialFile 对象创建 log::Reader,以读取文件
- S3 调用 log::Reader 的 ReadRecord 依次读取 record,如果和指定的 record 相同,就返回 true,没有相同的 record 就返回 false
SetCurrentFile 很简单,就是根据指定 manifest 文件编号,构造出 MANIFEST 文件名,并写入到 CURRENT 即可。
11.5 ApproximateOffsetOf()¶
函数声明:
uint64_tApproximateOffsetOf(Version* v, const InternalKey& ikey)
在指定的 version 中查找指定 key 的大概位置。 假设 version 中有 n 个 sstable 文件,并且落在了地 i 个 sstable 的 key 空间内,那么返回的位置= sstable1 文件大小 +sstable2 文件大小 + … + sstable (i-1) 文件大小 + key 在 sstable i 中的大概偏移。 可分为两段逻辑。
-
首先直接和 sstable 的 max key 作比较,如果 key > max key,直接跳过该文件,还记得 sstable 文件是有序排列的。 对于 level >0 的文件集合而言,如果如果 key < sstable 文件的 min key,则直接跳出循环,因为后续的 sstable 的 min key 肯定大于 key。
-
key 在 sstable i 中的大概偏移使用的是 Table:: ApproximateOffsetOf(target) 接口,前面分析过,它返回的是 Table 中>= target 的 key 的位置。
VersionSet 的相关函数 暂时分析到这里,compaction 部分后需单独分析。