天天看點

leveldb之log寫操作

當應用要插入一條記錄時,leveldb首先是将其寫入到log中,若成功,則繼續将其插入到memtable中。是以,當系統故障而memtable又沒有來得及将資料存放到記憶體中,那麼就可以通過log檔案來恢複資料,保證資料不會丢失。

由于log的讀比較複雜,是以将主要介紹log的寫操作。

在class DBImpl中主要有兩個與log相關的成員變量:log::Writer* log_; 和 WritableFile* logfile_;

其中log_用于向logfile_中增加一條記錄 ,logfile_主要用于對log檔案進行同步、重新整理等操作

1、Writer類

class Writer {
 public:
  explicit Writer(WritableFile* dest);
  ~Writer();

  Status AddRecord(const Slice& slice);

 private:
  WritableFile* dest_;  //以一個WritableFile對象作為Writer的成員,Writer則是将要插入的記錄插入到dest_中
  int block_offset_;       // 目前位置在Block中的偏移
  uint32_t type_crc_[kMaxRecordType + ];//CRC

  Status EmitPhysicalRecord(RecordType type, const char* ptr, size_t length);//調用Append寫入資料

};
           

Writer類對外隻提供了一個方法AddRecord()用于加入一條記錄,同時其中還有一個WritableFile成員變量,記錄最終是插入到WritableFile建立的檔案中的。

根據leveldb源碼可知,log檔案每次都是以32K的實體Block為機關進行操作的,是以log檔案可看作是由很多個連續的32K的Block組成的。插入一條資料時,首先确定資料在Block中的起始位置,然後不斷寫入到log檔案中。

Status Writer::AddRecord(const Slice& slice) {
  const char* ptr = slice.data();
  size_t left = slice.size();

  Status s;
  bool begin = true;
  do {
    const int leftover = kBlockSize - block_offset_;//目前Block中的剩餘空間
    assert(leftover >= );
    if (leftover < kHeaderSize) {//若剩餘空間比固定頭部要小,則要在一個新Block的開始寫入資料
      if (leftover > ) {
        // Fill the trailer (literal below relies on kHeaderSize being 7)
        assert(kHeaderSize == );
        dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));//尾部填充
      }
      block_offset_ = ;//塊内偏移置0
    }

    const size_t avail = kBlockSize - block_offset_ - kHeaderSize;//目前Block中,可用于填充資料的長度
    const size_t fragment_length = (left < avail) ? left : avail;//第一個要寫入Block的分段的長度

    RecordType type;
    const bool end = (left == fragment_length);//若end=1,則表明所有資料都可存放在目前Block中
    if (begin && end) {//根據begin和end确定目前記錄的類型type
      type = kFullType;
    } else if (begin) {
      type = kFirstType;
    } else if (end) {
      type = kLastType;
    } else {
      type = kMiddleType;
    }

    s = EmitPhysicalRecord(type, ptr, fragment_length);//将固定頭部和fragment_length長的分段寫入到log檔案dest_中
    ptr += fragment_length;//指向資料的指針向前移動已寫入長度
    left -= fragment_length;//剩餘待寫入長度減小
    begin = false;
  } while (s.ok() && left > );
  return s;
}
           

要寫入的記錄分為固定頭部和待寫入資料兩部分,其中固定頭部包括:CRC(4位元組)、記錄長度(2位元組)、type(1位元組)共7位元組。而待寫入資料一般是經過WriteBatch組織的一條記錄(主要包括type(kTypeValue或kTypeDeletion)、key、value)。

2、WritableFile類

class WritableFile {
 public:
  WritableFile() { }
  virtual ~WritableFile();

  virtual Status Append(const Slice& data) = ;//寫入記錄
  virtual Status Close() = ;//關閉檔案
  virtual Status Flush() = ;//重新整理檔案
  virtual Status Sync() = ;//同步檔案
};
           

WritableFile類隻是作為一個抽象基類,定義了一些純虛函數作為接口,最終作為父類被繼承。

leveldb中定義的一個子類為class PosixWritableFile:

class PosixWritableFile : public WritableFile {
 private:
  std::string filename_;//要操作的檔案名
  FILE* file_;//最終要操作的檔案

 public:

  virtual Status Append(const Slice& data) {
    size_t r = fwrite_unlocked(data.data(), , data.size(), file_);//調用fwrite将資料寫入到file_中
    return Status::OK();
  }
  virtual Status Close() {
    Status result;
    if (fclose(file_) != ) {//關閉檔案
      result = IOError(filename_, errno);
    }
    file_ = NULL;
    return result;
  }
  virtual Status Flush() {
    if (fflush_unlocked(file_) != ) {//重新整理檔案
      return IOError(filename_, errno);
    }
    return Status::OK();
  }
  virtual Status Sync() {//同步檔案
    // Ensure new files referred to by the manifest are in the filesystem.
    Status s = SyncDirIfManifest();
    if (fflush_unlocked(file_) !=  ||
        fdatasync(fileno(file_)) != ) {
      s = Status::IOError(filename_, strerror(errno));
    }
    return s;
  }
};
           

WritableFile類在寫入資料時不會對資料進行任何封裝、修改操作,而是直接将資料寫入到log檔案中。

是以我們一般插入一個Key-Value對時,首先會調用batch.Put(key, value);将其組織成一條記錄,然後調用Write::AddRecord(),在log檔案中找到合适的位置,同時為每一條記錄增加一個頭部,再将其寫入到log檔案中。然後調用WritableFile的Flush()、Sync()等方法來對log檔案進行操作。