leveldb源碼分析:Open啓動流程

leveldb概述

Leveldb 是一個持久化的KV存儲系統,主要將大部分數據存儲在磁盤上,在存儲數據的過程中,根據記錄的key值有序存儲,當然使用者也可以自定義Key大小比較函數,一個leveldb數據庫類似與一個操作系統文件夾,所有的有關數據庫內容都存儲在該文件夾下面,並提供Put,Delete和Get等方法去更改或查詢該數據庫,並且提供原子更新,同步寫(因爲leveldb默認情況下是異步寫,主要是通過操作系統的fsync等系統調用來實現的),支持數據快照,使得讀取操作不受寫操作影響並在讀過程中始終看到一致的數據等特性。根據官網數據,leveldb的隨機寫性能大大快於讀操作,適合寫多讀少的場景。後續特性待具體分析時具體分析。由於leveldb是由C++編寫的,大家需要熟悉一下C++的基礎知識。

leveldb的示例代碼

本文示例代碼如下:

#include "leveldb/db.h"
#include <cassert>
#include <iostream>

using namespace std;
using namespace leveldb;

int main(){
	leveldb::DB *db;
	leveldb::Options options;
	options.create_if_missing = true;
	leveldb::Status status = leveldb::DB::Open(options, "/root/testdb", &db);
	assert(status.ok());

	status = db->Put(WriteOptions(), "KeyNameExample", "ValueExample");
	assert(status.ok());
	string res;
	status = db->Get(ReadOptions(), "KeyNameExample", &res);
	assert(status.ok());
	cout<< res << endl;
	delete db;
	return 0;
}

主要涉及到了Open,Put,Get等操作。查看具體的內容分析

leveldb打開啓動流程

主要就是講述DB::Open的相關操作流程。

open函數的流程

按照示例代碼可知導入的模塊的內容是leveldb/db.h中的類DB,大家可自行查看該類,該類是一個基類,需要繼承並實現相關的方法,此時的Open是一個類的Static方法,實現的具體方法位於db/db_impl.cc文件中;

Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) {
  *dbptr = nullptr;

  DBImpl* impl = new DBImpl(options, dbname);                           // 新生產一個DBImpl類並賦值給dbptr指針
  impl->mutex_.Lock();                                                  // 查看源碼可知 該是一個線程鎖 實現原子操作
  VersionEdit edit;                                                     // 版本信息
  // Recover handles create_if_missing, error_if_exists
  bool save_manifest = false;
  Status s = impl->Recover(&edit, &save_manifest);                      // 先查數據庫在硬盤上的信息
  if (s.ok() && impl->mem_ == nullptr) {                                // 如果mem_ 指向的就是memtable 就是內存保存的數據庫的地址
    // Create new log and a corresponding memtable. 
    uint64_t new_log_number = impl->versions_->NewFileNumber();         //    新生成一個版本號
    WritableFile* lfile;
    s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),       // 生產一個新的寫文件
                                     &lfile);
    if (s.ok()) {                                                       // 如果創建成功則 先設置日誌版本號
      edit.SetLogNumber(new_log_number);
      impl->logfile_ = lfile;                                           // 初始化impl 相關內容
      impl->logfile_number_ = new_log_number;
      impl->log_ = new log::Writer(lfile);                              // 初始化 日誌
      impl->mem_ = new MemTable(impl->internal_comparator_);            // 重新生產一個MemTable保存數據
      impl->mem_->Ref();
    }
  }
  if (s.ok() && save_manifest) {                                        // 是否從舊版本的文件中恢復
    edit.SetPrevLogNumber(0);  // No older logs needed after recovery.
    edit.SetLogNumber(impl->logfile_number_);
    s = impl->versions_->LogAndApply(&edit, &impl->mutex_);             
  }
  if (s.ok()) {
    impl->DeleteObsoleteFiles();                                      // 刪除所有不是當前日誌文件的日誌文件 刪除所有未從某個級別引用的表文件
    impl->MaybeScheduleCompaction();                                  // 是否需要壓縮合並
  }
  impl->mutex_.Unlock();                                              // 釋放鎖
  if (s.ok()) {
    assert(impl->mem_ != nullptr);                                    // 判斷mem_不爲空
    *dbptr = impl;                                                    // 賦值
  } else {
    delete impl;                                                      // 如果出錯釋放新生成的impl
  }
  return s;                                                           // 返回狀態s
}

其中DBImpl就是實現了基類DB的虛方法,並實現了對應的功能,此時會先檢查對應的意見存在的版本信息,如果已經存在則嘗試是否需要從舊版本中恢復數據,然後如果不需要恢復則表示都是新創建的則進行數據的初始化,然後在設置日誌文件的配置參數,最後再調用清理函數和檢查是否需要合併的函數。

impl->Recover恢複函數
Status DBImpl::Recover(VersionEdit* edit, bool* save_manifest) {
  mutex_.AssertHeld();                                                      // 確保獲得了線程鎖

  // Ignore error from CreateDir since the creation of the DB is
  // committed only when the descriptor is created, and this directory
  // may already exist from a previous failed creation attempt.
  env_->CreateDir(dbname_);                                                 // env_是根據不同平臺獲取得到的相關函數操作 是在DBImpl初始化時根據傳入參數創建
  assert(db_lock_ == nullptr);
  Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);              // 實現對文件的加鎖, 如果其他文件以及鎖住了該文件則返回鎖是啊比
  if (!s.ok()) {
    return s;
  }

  if (!env_->FileExists(CurrentFileName(dbname_))) {                        // 檢查CURRENT文件是否存在
    if (options_.create_if_missing) {                                       // 如果參數設置的是如果不存在則創建
      s = NewDB();                                                          // 生成一個DB 生成相關的文件
      if (!s.ok()) {                                                        // 如果不成功則直接返回狀態
        return s;
      }
    } else {
      return Status::InvalidArgument(
          dbname_, "does not exist (create_if_missing is false)");        // 返回異常錯誤
    }
  } else {
    if (options_.error_if_exists) {
      return Status::InvalidArgument(dbname_,
                                     "exists (error_if_exists is true)");       // 通過傳入的數據是否拋錯誤
    }
  }

  s = versions_->Recover(save_manifest);                                //  根據save_manifest 來是否生成新的版本信息 根據讀入的最後的MANIFEST存入的最後的數據
  if (!s.ok()) {
    return s;
  }
  SequenceNumber max_sequence(0);                                       // 獲取序列號

  // Recover from all newer log files than the ones named in the
  // descriptor (new log files may have been added by the previous
  // incarnation without registering them in the descriptor).
  //
  // Note that PrevLogNumber() is no longer used, but we pay
  // attention to it in case we are recovering a database
  // produced by an older version of leveldb.
  const uint64_t min_log = versions_->LogNumber();                      // 獲取日誌號
  const uint64_t prev_log = versions_->PrevLogNumber();                 // 先前日誌號
  std::vector<std::string> filenames;
  s = env_->GetChildren(dbname_, &filenames);                           // 獲取對應的子文件
  if (!s.ok()) {
    return s;
  }
  std::set<uint64_t> expected;
  versions_->AddLiveFiles(&expected);
  uint64_t number;
  FileType type;
  std::vector<uint64_t> logs;
  for (size_t i = 0; i < filenames.size(); i++) {   
    if (ParseFileName(filenames[i], &number, &type)) {
      expected.erase(number);
      if (type == kLogFile && ((number >= min_log) || (number == prev_log)))
        logs.push_back(number);                                       // 打入數據
    }
  }
  if (!expected.empty()) {
    char buf[50];
    snprintf(buf, sizeof(buf), "%d missing files; e.g.",
             static_cast<int>(expected.size()));
    return Status::Corruption(buf, TableFileName(dbname_, *(expected.begin())));
  }

  // Recover in the order in which the logs were generated
  std::sort(logs.begin(), logs.end());                               // 排序
  for (size_t i = 0; i < logs.size(); i++) {
    s = RecoverLogFile(logs[i], (i == logs.size() - 1), save_manifest, edit,
                       &max_sequence);                              // 重新生成文件
    if (!s.ok()) {
      return s;
    }

    // The previous incarnation may not have written any MANIFEST
    // records after allocating this log number.  So we manually
    // update the file number allocation counter in VersionSet.
    versions_->MarkFileNumberUsed(logs[i]);                       // 標記該文件被使用過
  }

  if (versions_->LastSequence() < max_sequence) {
    versions_->SetLastSequence(max_sequence);                     // 設置最新序列號
  }

  return Status::OK();
}

主要通過文件鎖,獲取對數據庫恢復的權限,如果有已經存在的數據庫則讀入當前存在的數據庫相關信息,來判斷是否需要重新建立0層level對應的數據,是否需要從日誌文件中恢復數據,從日誌恢復數據的操作主要是RecoverLogFile函數執行;

Status DBImpl::RecoverLogFile(uint64_t log_number, bool last_log,
                              bool* save_manifest, VersionEdit* edit,
                              SequenceNumber* max_sequence) {
  struct LogReporter : public log::Reader::Reporter {
    Env* env;
    Logger* info_log;
    const char* fname;
    Status* status;  // null if options_.paranoid_checks==false
    void Corruption(size_t bytes, const Status& s) override {
      Log(info_log, "%s%s: dropping %d bytes; %s",
          (this->status == nullptr ? "(ignoring error) " : ""), fname,
          static_cast<int>(bytes), s.ToString().c_str());
      if (this->status != nullptr && this->status->ok()) *this->status = s;
    }
  };

  mutex_.AssertHeld();                                            // 獲取線程鎖

  // Open the log file
  std::string fname = LogFileName(dbname_, log_number);           // 打開日誌文件
  SequentialFile* file;
  Status status = env_->NewSequentialFile(fname, &file);          // 生成新的序列號文件
  if (!status.ok()) {
    MaybeIgnoreError(&status);
    return status;
  }

  // Create the log reader.
  LogReporter reporter;                                          // 生成一個reporter 並初始化參數
  reporter.env = env_;
  reporter.info_log = options_.info_log;
  reporter.fname = fname.c_str();
  reporter.status = (options_.paranoid_checks ? &status : nullptr);
  // We intentionally make log::Reader do checksumming even if
  // paranoid_checks==false so that corruptions cause entire commits
  // to be skipped instead of propagating bad information (like overly
  // large sequence numbers).
  log::Reader reader(file, &reporter, true /*checksum*/, 0 /*initial_offset*/);
  Log(options_.info_log, "Recovering log #%llu",
      (unsigned long long)log_number);

  // Read all the records and add to a memtable
  std::string scratch;
  Slice record;
  WriteBatch batch;
  int compactions = 0;
  MemTable* mem = nullptr;
  while (reader.ReadRecord(&record, &scratch) && status.ok()) {             // 讀數據
    if (record.size() < 12) {
      reporter.Corruption(record.size(),
                          Status::Corruption("log record too small"));      // 打印信息
      continue;
    }
    WriteBatchInternal::SetContents(&batch, record);                        // 設置批量寫入

    if (mem == nullptr) {
      mem = new MemTable(internal_comparator_);                             // 生成一個新的MemTable
      mem->Ref();
    }
    status = WriteBatchInternal::InsertInto(&batch, mem);                   // 插入信息
    MaybeIgnoreError(&status);
    if (!status.ok()) {
      break;
    }
    const SequenceNumber last_seq = WriteBatchInternal::Sequence(&batch) +
                                    WriteBatchInternal::Count(&batch) - 1;      // 統計計數
    if (last_seq > *max_sequence) {
      *max_sequence = last_seq;                                                 // 判斷最後一個是否超過最大值
    }

    if (mem->ApproximateMemoryUsage() > options_.write_buffer_size) {         // 檢查是否超過MemTable的閾值
      compactions++;
      *save_manifest = true;
      status = WriteLevel0Table(mem, edit, nullptr);                          // 寫入0層level的數據
      mem->Unref();
      mem = nullptr;
      if (!status.ok()) {
        // Reflect errors immediately so that conditions like full
        // file-systems cause the DB::Open() to fail.
        break;
      }
    }
  }

  delete file;

  // See if we should keep reusing the last log file.
  if (status.ok() && options_.reuse_logs && last_log && compactions == 0) {     // 如果狀態ok 重用log last_log有內容 compactions爲0 
    assert(logfile_ == nullptr);
    assert(log_ == nullptr);
    assert(mem_ == nullptr);
    uint64_t lfile_size;
    if (env_->GetFileSize(fname, &lfile_size).ok() &&
        env_->NewAppendableFile(fname, &logfile_).ok()) {                      // 獲取文件大小 獲取文件名
      Log(options_.info_log, "Reusing old log %s \n", fname.c_str());
      log_ = new log::Writer(logfile_, lfile_size);                            // 創建一個寫log
      logfile_number_ = log_number;
      if (mem != nullptr) {                                                    // 設置mem_
        mem_ = mem;
        mem = nullptr;
      } else {
        // mem can be nullptr if lognum exists but was empty.
        mem_ = new MemTable(internal_comparator_);
        mem_->Ref();
      }
    }
  }

  if (mem != nullptr) {                                                       // 如果mem 不爲空 則創建0層level
    // mem did not get reused; compact it.
    if (status.ok()) {
      *save_manifest = true;
      status = WriteLevel0Table(mem, edit, nullptr);
    }
    mem->Unref();
  }

  return status;
}

至此,主要的leveldb的數據庫打開操作就執行完成了。

總結

本文主要概述了leveldb的基本特性,並概述了leveldb啓動Open函數的源碼的執行流程,裏面有關環境變量等類參數的初始化,需要大家在熟悉一下有關C++基礎之後,再去查看相關的線程鎖等實現細節。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章