LevelDB源碼解讀——數據庫開啓、讀取與存入

本章主要介紹對LevelDB基礎操作Open,Put,Get等操作,熟悉具體的數據讀寫處理流程。
先放一個基本的LevelDB的基礎操作demo。demo實現的就是首先打開一個數據庫,然後向其插入一個KV數據,隨後讀取出來。

int main(int argc,char* argv[]){
    leveldb::DB* ptr = nullptr;
    const std::string name = "./myleveldata.ldb";
    leveldb::Options option;
    option.create_if_missing = true;
    //open
    leveldb::Status status = leveldb::DB::Open(option,name,&ptr);
    if(!status.ok()){
        std::cerr<<"open db error "<<status.ToString()<<std::endl;
        return 1;
    }
    assert(ptr != nullptr);
    const std::string key = "myfirstkey";
    const std::string value = "myfirstvalue";
    //put
    {
        leveldb::WriteOptions writeoptions;
        writeoptions.sync = true;    //leveldb默認寫是異步的,這裏打開syncx同步
        status = ptr->Put(writeoptions,key,value);
        if(!status.ok()){
            std::cerr<<"write error";
            return 1;
        }
    }
    //get
    {
        leveldb::ReadOptions getOptions;
        std::string rdVal;
        status = ptr->Get(getOptions, key, &rdVal);
        if (!status.ok()) {
            std::cerr << "get data error" << std::endl;
            return 0;
        }
        assert(value == rdVal);
        std::cout<<"happy ending ";
    }
    delete ptr;
    return 0;
}

可以看出,整體數據操作都是圍繞DB*,首先在打開數據庫的時候會返回一個DB*,然後圍繞其做增刪改查操作。DB類是一個抽象類,但是其對於純虛函數都有提供默認實現,繼承其的子類也還是要重寫這些虛函數。

class LEVELDB_EXPORT DB {
 public:
  // 以下函數具體實現在db_impl.cc
  // 打開數據庫,返回dbptr
  static Status Open(const Options& options, const std::string& name,
                     DB** dbptr);
  DB() = default;
  DB(const DB&) = delete;
  DB& operator=(const DB&) = delete;
  virtual ~DB();
  // 注意這裏純虛函數,也可以提供默認實現
  // 向數據庫中插入key-value對
  virtual Status Put(const WriteOptions& options, const Slice& key,
                     const Slice& value) = 0;
  // 數據庫中刪除key
  virtual Status Delete(const WriteOptions& options, const Slice& key) = 0;
  // 數據庫更新
  virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0;
  // 數據庫中讀取key,如果有則返回value,如果沒有則保持value不變,status=Status::IsNotFount()
  virtual Status Get(const ReadOptions& options, const Slice& key,
                     std::string* value) = 0;
  ...
};

數據庫Open流程

Open流程較爲容易,簡單而言就是根據輸入的dbname,找到對應的磁盤文件,將其數據恢復到內存中,同時設置log和manifest等,將這些數據都放到DBimpl*對象中,並返回其爲進一步的讀寫操作。

Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) {
  *dbptr = nullptr;
  //構造一個impl,並調用Recover
  DBImpl* impl = new DBImpl(options, dbname);
  //接下來的操作是從日誌中恢復數據庫的內容(將數據加載到內存中),因爲裏面要對impl進行賦值,所以要先加鎖
  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) {
    // Create new log and a corresponding memtable.
    // 創建新log和memtable(並且要自己增加reference)
    uint64_t new_log_number = impl->versions_->NewFileNumber();
    WritableFile* lfile;
    // 將數據從磁盤文件中加載出來賦給impl
    s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
                                     &lfile);
    if (s.ok()) {
      edit.SetLogNumber(new_log_number);
      impl->logfile_ = lfile;
      impl->logfile_number_ = new_log_number;
      impl->log_ = new log::Writer(lfile);
      impl->mem_ = new MemTable(impl->internal_comparator_);
      impl->mem_->Ref();
    }
  }
  if (s.ok() && save_manifest) {
    edit.SetPrevLogNumber(0);  // No older logs needed after recovery.
    edit.SetLogNumber(impl->logfile_number_);
    // 設置Manifest爲當前版本
    s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
  }
  if (s.ok()) {
    impl->DeleteObsoleteFiles();
    impl->MaybeScheduleCompaction();
  }
  impl->mutex_.Unlock();
  if (s.ok()) {
    assert(impl->mem_ != nullptr);
    *dbptr = impl;
  } else {
    delete impl;
  }
  return s;
}

數據庫Put、Delete操作

put操作是將key-value插入到數據庫中。首先會調用DBImpl::Put函數,而DBImpl::Put直接調用父類的DB::Put ,DB::Put中新建一個WriteBatch對象,調用WriteBatch::Put函數,最後在調用DBImpl::Write函數。下圖所示的是一個Batch對象的結構。
batch對象結構
對於WriteBatch對象的構建都是在WriteBatch::Put函數中進行的,函數的作用就是將key和value值都放在batch對象中。這裏有一點需要說明的是,數據庫的寫入操作和刪除操作其實是相似的操作,delete操作其實就是一個value爲空的put操作,寫入操作的type是kTypeValue,刪除操作的type是kTypeDeletion。

Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
  WriteBatch batch;
  batch.Put(key, value);
  return Write(opt, &batch);
}

Status DB::Delete(const WriteOptions& opt, const Slice& key) {
  WriteBatch batch;
  batch.Delete(key);
  return Write(opt, &batch);
}

接下來的DBImlp::Write函數操作至關重要,這個函數的作用是具體寫入,其能體現出LevelDB寫性能卓越的原因。具體過程如下:

  1. 首先初始化一個Writer對象,Writer對象其實是batch對象的一個封裝數據結構。
  2. 然後用一個deque隊列同步線程,目的是合併多個batch一起寫入磁盤,將多個操作合併成一個批插入操作,這樣能有效減少寫磁盤的次數,提高系統的寫性能。這裏還有一個小技巧,利用條件變量和互斥量來實現這個功能。不同的線程將Writer對象插入deque隊列,如果發現隊列中已經存在Writer對象(也就是說對象非隊列頭部)則wait等待。直到等到信號纔會被一起BuildBatchGroup寫入log和MemTable。這裏給一個例子。這裏有五個線程,A,B,C,D,E,每個線程都擁有一個Writer想要寫入。一開始deque隊列爲空,A線程獲取mutex,然後deque中push進去,因爲其爲隊列首部,則繼續執行下去,由於其一直佔着mutex,其他線程無法插入到deque中,所以線程A執行的BuildBatchGroup,SetSequence等操作都是隻包含自己,然而在寫日誌之前,線程A釋放掉mutex,這時候其他線程就可以爭搶mutex並加入deque中,但是由於並不是隊列頭部而條件變量等待,這時候又會釋放掉mutex,其他線程繼續爭搶,最後B,C,D,E都會加入deque,但是順序很可能不一樣。接着線程A會繼續寫入log和MemTable,然後會調用signal喚醒隊列中下一個線程(假設爲C),線程C喚醒後,也是同樣的操作,只是此時隊列中還剩下B,D,E線程,這時候BuildBatchGroup就可以將deque中的所有Writer合併爲一個batch,一次性的寫入log和MemTable,最後循環pop隊列頭部元素,並比較是否爲last_writer,比較完成(E == last_writer)後即退出。
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
  //初始化Writer
  Writer w(&mutex_);
  w.batch = updates;
  w.sync = options.sync;
  w.done = false;

  //加鎖爲了線程同步(條件變量),合併多個batch一起寫入磁盤,合併爲一個批插入操作,這也是寫操作高性能的關鍵所在
  MutexLock l(&mutex_);
  writers_.push_back(&w);
  while (!w.done && &w != writers_.front()) {
    w.cv.Wait();
  }
  if (w.done) {
    return w.status;
  }

  // May temporarily unlock and wait.
  Status status = MakeRoomForWrite(updates == nullptr);
  uint64_t last_sequence = versions_->LastSequence();
  Writer* last_writer = &w;
  if (status.ok() && updates != nullptr) {  // nullptr batch is for compactions
    //將writers_隊列中的所有batch合併,一起寫入。
    WriteBatch* updates = BuildBatchGroup(&last_writer);
    WriteBatchInternal::SetSequence(updates, last_sequence + 1);
    last_sequence += WriteBatchInternal::Count(updates);

    // Add to log and apply to memtable.  We can release the lock
    // during this phase since &w is currently responsible for logging
    // and protects against concurrent loggers and concurrent writes
    // into mem_.
    {
      mutex_.Unlock();
      //數據順序寫入log日誌
      status = log_->AddRecord(WriteBatchInternal::Contents(updates));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      if (status.ok()) {
        //數據插入到memTable
        status = WriteBatchInternal::InsertInto(updates, mem_);
      }
      mutex_.Lock();
      if (sync_error) {
        // The state of the log file is indeterminate: the log record we
        // just added may or may not show up when the DB is re-opened.
        // So we force the DB into a mode where all future writes fail.
        RecordBackgroundError(status);
      }
    }
    if (updates == tmp_batch_) tmp_batch_->Clear();

    versions_->SetLastSequence(last_sequence);
  }

  while (true) {
    Writer* ready = writers_.front();
    writers_.pop_front();
    if (ready != &w) {
      ready->status = status;
      ready->done = true;
      ready->cv.Signal();
    }
    if (ready == last_writer) break;
  }

  // Notify new head of write queue
  if (!writers_.empty()) {
    writers_.front()->cv.Signal();
  }

  return status;
}

數據庫Get操作

Get操作也是很重要的一塊,雖然主體流程很清晰,但是裏面有很多需要注意的點,花費了工程師很多時間優化。比如這裏的LookupKey的作用,還有在MemTable中查找的過程等。下一章我會分析MemTable和Version的結構、讀寫過程。

Status DBImpl::Get(const ReadOptions& options, const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  SequenceNumber snapshot;
  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
  } else {
    snapshot = versions_->LastSequence();
  }
  //MemTable
  MemTable* mem = mem_;
  //Immutable MemTable
  MemTable* imm = imm_;
  //current version
  Version* current = versions_->current();
  //要自己增加引用計數
  mem->Ref();
  if (imm != nullptr) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats;

  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    // 讀取順序,Memtable -> Immutable Memtable -> Current Version
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) {
      // Done
    } else if (imm != nullptr && imm->Get(lkey, value, &s)) {
      // Done
    } else {
      s = current->Get(options, lkey, value, &stats);
      have_stat_update = true;
    }
    mutex_.Lock();
  }

  //如果是從磁盤中獲取的數據,則有可能出發Comapction合併操作
  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction();
  }
  // 讀取完成後引用計數減一
  mem->Unref();
  if (imm != nullptr) imm->Unref();
  current->Unref();
  return s;
}

參考博客:

  1. https://leveldb-handbook.readthedocs.io/zh/latest/rwopt.html
  2. https://www.cnblogs.com/ym65536/p/7720105.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章