跳转至

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 部分后需单独分析。