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 + 1];//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 >= 0);
    if (leftover < kHeaderSize) {//若剩余空间比固定头部要小,则要在一个新Block的开始写入数据
      if (leftover > 0) {
        // Fill the trailer (literal below relies on kHeaderSize being 7)
        assert(kHeaderSize == 7);
        dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));//尾部填充
      }
      block_offset_ = 0;//块内偏移置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 > 0);
  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) = 0;//写入记录
  virtual Status Close() = 0;//关闭文件
  virtual Status Flush() = 0;//刷新文件
  virtual Status Sync() = 0;//同步文件
};

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(), 1, data.size(), file_);//调用fwrite将数据写入到file_中
    return Status::OK();
  }
  virtual Status Close() {
    Status result;
    if (fclose(file_) != 0) {//关闭文件
      result = IOError(filename_, errno);
    }
    file_ = NULL;
    return result;
  }
  virtual Status Flush() {
    if (fflush_unlocked(file_) != 0) {//刷新文件
      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_) != 0 ||
        fdatasync(fileno(file_)) != 0) {
      s = Status::IOError(filename_, strerror(errno));
    }
    return s;
  }
};

WritableFile类在写入数据时不会对数据进行任何封装、修改操作,而是直接将数据写入到log文件中。
因此我们一般插入一个Key-Value对时,首先会调用batch.Put(key, value);将其组织成一条记录,然后调用Write::AddRecord(),在log文件中找到合适的位置,同时为每一条记录增加一个头部,再将其写入到log文件中。然后调用WritableFile的Flush()、Sync()等方法来对log文件进行操作。

你可能感兴趣的:(leveldb)