標籤:

LevelDB源碼解析13. 新建DB

LevelDB源碼解析13. 新建DB

來自專欄 CodeIt1 人贊了文章

看了這麼多代碼,接下來將要開始接觸比較核心的部分了。這一章將要回答第一個 問題:

當一切都是0的時候,也就是什麼都還沒有的時候,打開DB的流程是什麼樣的?

這裡我們先只考慮DB還未存的這種場景。

看代碼就是:提出問題,猜測,推翻猜測,尋找答案

main.c

假設我們有一段代碼只做如下事情。這段代碼做的事情就是打開DB。其他任何事情都不做。那麼假設此時DB還未存在

leveldb::DB *db; leveldb::Options options; _LOG << "end of construct options" << std::endl; options.create_if_missing = true; leveldb::Status status = leveldb::DB::Open(options, "testdb", &db); assert(status.ok()); delete db; return 0;

Open

接下來將會進入到DB::Open()介面上。

Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) {_BLOG; *dbptr = NULL; DBImpl* impl = new DBImpl(options, dbname); .....}

那麼接下來再看一下DBImpl()構造函數做了些什麼?

DBImpl構造函數

// DB::Open()->DBImpl::DBImpl()DBImpl::DBImpl(const Options& raw_options, const std::string& dbname) : // 初始化env環境變數 env_(raw_options.env), // 初始化key比較器 internal_comparator_(raw_options.comparator), // 初始化過濾策略 internal_filter_policy_(raw_options.filter_policy), // 設置option:1. 參數. 2. info_log 3. block_cache options_(SanitizeOptions(dbname, &internal_comparator_, &internal_filter_policy_, raw_options)), // 是否有info_log_ owns_info_log_(options_.info_log != raw_options.info_log), // 是否有option中的cache. owns_cache_(options_.block_cache != raw_options.block_cache), // 資料庫的名字 dbname_(dbname), // table_cache指的是sst文件的index部分的cache table_cache_(new TableCache(dbname_, options_, TableCacheSize(options_))), // 資料庫的鎖 db_lock_(nullptr), // 是否關閉: Q: 這裡為什麼用指針?而不是用bool變數 shutting_down_(nullptr), // 後台線程信號 background_work_finished_signal_(&mutex_), // 活躍的mem mem_(nullptr), // 不能修改的mem imm_(nullptr), // WAL journal文件句柄,類似於FILE指針 logfile_(nullptr), // WAL文件序號 logfile_number_(0), // WAL文件寫者 log_(nullptr), // 隨機數種子 seed_(0), // 臨時批量寫 tmp_batch_(new WriteBatch), // 後台合併調度 background_compaction_scheduled_(false), // 手動合併 manual_compaction_(nullptr), // 生成一個空的版本對象 versions_(new VersionSet(dbname_, &options_, table_cache_, &internal_comparator_)) { // 設置has_imm_為nullptr. has_imm_.Release_Store(nullptr);}

這裡先不去管Release_Store語句,只需要知道,從閱讀代碼的角度上來講,相當於賦值操作。後面再專門研究內存壁障

其中在準備Option的時候,操作如下:

// DB::Open()->DBImpl::DBImpl()->SanitizeOptions()// Fix user-supplied options to be reasonabletemplate <class T, class V>static void ClipToRange(T* ptr, V minvalue, V maxvalue) { if (static_cast<V>(*ptr) > maxvalue) *ptr = maxvalue; if (static_cast<V>(*ptr) < minvalue) *ptr = minvalue;}/* * 修改option裡面的各種設定: * 1. 修改參數範圍 * 2. 設置info_log * 3. 設置block_cache */Options SanitizeOptions(const std::string& dbname, const InternalKeyComparator* icmp, const InternalFilterPolicy* ipolicy, const Options& src) { // 基於傳進來的option Options result = src; // 設置key比較器 result.comparator = icmp; // 過濾器, 先不管這個用來做什麼 result.filter_policy = (src.filter_policy != nullptr) ? ipolicy : nullptr; // 這裡是把參數歸一化,過大的數放到max_value // 太小的數放到min_value // 介於最大和最小之間的不做處理 ClipToRange(&result.max_open_files, 64 + kNumNonTableCacheFiles, 50000); ClipToRange(&result.write_buffer_size, 64<<10, 1<<30); ClipToRange(&result.max_file_size, 1<<20, 1<<30); ClipToRange(&result.block_size, 1<<10, 4<<20); // 中間操作記錄,出錯信息等等 // 會把中間步驟與信息都寫到這個文件裡面 // 這個文件放置的位置是dbname/info_log if (result.info_log == nullptr) { // 如果還沒有日誌指針,那麼創建相應的目錄 // Open a log file in the same directory as the db src.env->CreateDir(dbname); // In case it does not exist // 把原來的LOG文件重命名 src.env->RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname)); // 生成新的LOG文件 Status s = src.env->NewLogger(InfoLogFileName(dbname), &result.info_log); // 如果不成功 if (!s.ok()) { // No place suitable for logging // 那麼依舊使用nullptr. result.info_log = nullptr; } } // 如果沒有block_cache // block_cache就是用來存放sst文件裡面的block數據部分 // table_cache是用來存放sst文件裡面的index cache部分 // 這裡並沒有設置table_cache,後面可以看一下 // table_cache是在哪裡處理的。 if (result.block_cache == nullptr) { result.block_cache = NewLRUCache(8 << 20); } return result;}

準備VersionSet

DBImpl的時候會去初始化VersionSet,初始化代碼如下:

// DB::Open()->DBImpl::DBImpl()->VersionSet::VersionSet()DBImpl::DBImpl(const Options& raw_options, const std::string& dbname) : ... // 生成一個空的版本對象 versions_(new VersionSet(dbname_, &options_, table_cache_, &internal_comparator_)) { // 設置has_imm_為nullptr. has_imm_.Release_Store(nullptr);}// VersionSet的構造函數VersionSet::VersionSet(const std::string& dbname, const Options* options, TableCache* table_cache, const InternalKeyComparator* cmp) // 複製env : env_(options->env), // 設置dbname dbname_(dbname), // 設置option options_(options), // 設置table_cache_ table_cache_(table_cache), // 設置key比較器 icmp_(*cmp), // 接下來文件的編號 // 這裡使用了新生成的DB的編號 next_file_number_(2), // 這裡只是簡單地初始化為0 // 實際上在新生成DB的時候,編號已經是1了 manifest_file_number_(0), // Filled by Recover() // 用戶insert key/value的時候,會使用的seq last_sequence_(0), // WAL日誌文件使用的編號0 log_number_(0), // 前一個WAL日誌文件的編號 prev_log_number_(0), descriptor_file_(nullptr), descriptor_log_(nullptr), // 這個類似於自己寫鏈表裡面的dummy head. dummy_versions_(this), // 指向當前的version. // 當前version還沒有了 current_(nullptr) { // append一個新的versoin,放到雙向鏈表中 // 並且將current指向這個新來的version. AppendVersion(new Version(this));}

總結一下VersionSet所做的事情: 1. 初始化各種序號,這個序號需要和NewDB()序號保持一致。 2. 生成含dummy head的雙向鏈表 3. 初始化versioncurrent_指針

Recover

雖然前面做好了各種資料庫的準備工作。比如info_log_, table_cache_, block_cache_等等內存變數。但是還是需要考慮到:在打開一個DB的時候,如果這個DB已經存在了應該怎麼辦?

LevelDB並沒有將DB不存在DB已存在通過不同的方式來處理。這兩個邏輯都是通過Recovery進行統一的。那麼又是如何統一的呢?

在這裡我們只考慮打開的資料庫不存在的情況。

Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) { // DBImpl()構造函數 ... VersionEdit edit; // Recover handles create_if_missing, error_if_exists bool save_manifest = false; Status s = impl->Recover(&edit, &save_manifest); ...}

step1. 創建目錄

// DB::Open()->DBImpl::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. // 這裡直接忽略CreateDir失敗的情況。因為DB的創建只會在描述符被創建之後 // 再進行提交,並且這個目錄有可能是上次創建DB失敗之後留下來的。 env_->CreateDir(dbname_); // 因為剛創建DB // DB相關的文件鎖肯定還不存在 assert(db_lock_ == nullptr); // 接下來創建文件鎖 Status s = env_->LockFile(LockFileName(dbname_), &db_lock_); // 如果失敗,或者已經被上鎖,說明已經有人在使用DB了 // 直接退出 if (!s.ok()) { return s; } ....

step 2. 新的DB

是否需要創建新的DB由兩個條件來決定:

  1. ${dbname}/CURRENT文件是否存在
  2. create_if_missing是否需要創建

對於新創建的DB而言,CURRENT文件肯定是不存在的。那麼就需要創建了。

// DB::Open()->DBImpl::Recover()Status DBImpl::Recover(VersionEdit* edit, bool *save_manifest) { // .. 創建目錄,準備文件鎖 // 如果DB目錄裡面的CURRENT文件不存在 if (!env_->FileExists(CurrentFileName(dbname_))) { // 如果不存在的時候,需要創建DB if (options_.create_if_missing) { s = NewDB(); if (!s.ok()) { return s; } // 否則報錯 } else { return Status::InvalidArgument( dbname_, "does not exist (create_if_missing is false)"); } } else { // 如果CURRENT文件已經存在,說明DB已經存在 // 根據option來決定是否要報錯 if (options_.error_if_exists) { return Status::InvalidArgument( dbname_, "exists (error_if_exists is true)"); } } ... ...}

NewDB()

總結一下NewDB()

* 1. 初始化各種序列號 * 0給WAL日誌 * 1給manifest文件 * 2給將來要生成的新的文件編號 * 2. 利用WAL日誌文件將new_db這個版本編輯器dump到版本 * WAL日誌manifest文件中 * 3. 檢查是否寫入成功,如果寫入成功,那麼更新CURRENT文件 * 如果失敗,那麼刪除manifest文件

代碼如下:

// DB::Open()->DBImpl::Recover()->DBImpl::NewDB()Status DBImpl::NewDB() { // 生成一個空的版本編輯器 VersionEdit new_db; // 設置DB name new_db.SetComparatorName(user_comparator()->Name()); // 設置WAL編號: 也就是說WAL log把編號0佔用了 new_db.SetLogNumber(0); // 設置接下來的文件編號 // 接下來新的文件會使用編號2 // 那麼中間的1呢?下面可以看到1是被Manifest文件看到了。 new_db.SetNextFile(2); // 用戶提交key/value時的編號 new_db.SetLastSequence(0); // manifest文件的編號,這裡新生成的DB裡面把1佔用掉了。 const std::string manifest = DescriptorFileName(dbname_, 1); // 接下來把這個生成新DB的操作通過寫WAL日誌的方式 // 寫到manifest文件中 WritableFile* file; // manifest文件也是一個WAL文件 // 這裡是生成相應的文件句柄 Status s = env_->NewWritableFile(manifest, &file); if (!s.ok()) { return s; } // 把生成DB的操作寫入到manifest文件中 { log::Writer log(file); std::string record; new_db.EncodeTo(&record); s = log.AddRecord(record); if (s.ok()) { s = file->Close(); } } // 寫入完成之後一,釋放資源 delete file; if (s.ok()) { // 如果寫入成功,那麼將CURRENT文件指向當前的新的manifest文件 // Make "CURRENT" file that points to the new manifest file. s = SetCurrentFile(env_, dbname_, 1); } else { // 如果失敗,那麼就取消這個記錄 env_->DeleteFile(manifest); } return s;}

接下來關於Recover的具體步驟可以看一下:

zhuanlan.zhihu.com/p/35


推薦閱讀:

LevelDB源碼解析5. 數據完整性
LevelDB源碼解析11.文件序號
LevelDB源碼解析10.創建VersionSet
LevelDB源碼解析3. 基本思路
LevelDB源碼解析2. Visual Studio Code

TAG:源代碼 | LevelDB |