leveldb深度剖析-存储流程(1)

上一篇介绍了leveldb初始化流程,本篇开始介绍存储流程。对于leveldb来说,删除数据实际是插入数据,只是将类型设置为删除即可,并不会真正将数据从磁盘中删除.那么什么时候真正删除呢?在压缩过程中,具体在压缩流程中介绍。

一、插入

1.1、总体流程图

调用接口Put即可完成插入数据。那么内部实现是什么呢?首先来看一下总体流程图:

说明:

1) 调用接口Put插入数据,首先会先保存到.log文件文件中,这样做的好处是当异常重启保证数据不丢失 。

2) 插入到MemTable中,我们在之前篇章有介绍到MemTable底层实现是跳表,leveldb控制了MemTable使用内存空间大小,当插入数据使MemTable占用内存达到了门限值则会将MemTable转换成Immutable MemTable,反之亦然。

3)leveldb会创建一个新线程会将Immutable MemTable中的数据写到入level0文件中,写入完成后释放内存,紧接着判断是否需要进行压缩处理。

1.2、批任务

以下是Put整体流程图,相对比较简单.但是leveldb在内部却做了很复杂操作,leveldb在调用Put后会将key-value封装成WriteBatch(批任务),然后调用Write方法,将WriteBatch投递到生产者队列中,最后由消费者进行处理。

 1.2.1、批任务格式

一个WriteBatch是有内部存储格式,具体存储格式如下说是

  /**
   * 成员rep_ 内存表现形式
   *
   *   0                   1                   2                   3           
   *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                                                               |
   *   |                        Sequnce number                         |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                        count                                  |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                                                               |
   *   ~                    type-key-value (不定长)                     ~
   *   |                                                               |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                                                               |
   *   ~                    type-key-value (不定长)                     ~
   *   |                                                               |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   */
字段名类型大小说明
Sequnce number8字节序列号,每个WriteBatch都有唯一的序号
count4字节代表type-key-value有多少个,一个WriteBatch可以同时操作多个key-value.固定大小,按照小端序存储
type-key-value可以长度1)type(1字节)取值: kTypeDeletion(删除)和kTypeValue(插入)
2)key的存储: 先存储key-length再存储key实际内容,其中key-length进行7bit数字压缩出存储
3)valu的存储方式和key一样,当类型为kTypeDeletion时value部分为空

具体参考代码为WriteBatch::Write,源码比较简单这里不在贴出。

1.2.2、代码实现

/**
 * 写操作
 * @param options  写操作选项
 * @param my_batch 批任务
 */
Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) {
  Writer w(&mutex_); // 结构Writer在本文件 创建条件变量
  w.batch = my_batch;
  w.sync = options.sync;
  w.done = false;

  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(my_batch == NULL);
  
  uint64_t last_sequence = versions_->LastSequence();//表示上次已经使用的序列号
  Writer* last_writer = &w;
  
  if (status.ok() && my_batch != NULL) {  // NULL batch is for compactions
    WriteBatch* updates = BuildBatchGroup(&last_writer);//将多个WriteBatch合并成一个WriteBatch
    WriteBatchInternal::SetSequence(updates, last_sequence + 1);
    last_sequence += WriteBatchInternal::Count(updates); //下一个批任务的起始序号为last_sequence+1

    // 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();
      //先将record写入到文件中 再将record插入到mem_表中
      // 1、Contents方法是返回Slice对象
      // 2、此处AddRecord方法调用log_writer.cc中AddRecord方法
      status = log_->AddRecord(WriteBatchInternal::Contents(updates));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      // 只有在写入文件成功后才 插入到mem_表中
      if (status.ok()) {
        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;
}

说明:

1) leveldb支持多线程访问db对象,这里采用生产者消费者模式进行同步访问

2) 这里有一个方法MakeRoomForWrite该方法做了很多内容,主要确保有可用内存空间来保证当前WriteBatch写成功。如何保证内存空间可用呢? 例如:将MemTable变成Immutable MemTable以及进行压缩处理。此函数是重点函数,可参考本篇《leveldb深度剖析-压缩流程》

3) 为了提升性能,leveldb采用尽可能多在同一个线程内处理多个WriteBatch,所以会尝试将多个WriteBatch进行合并,具体函数为BuildBatchGroup

4) 根据合并后的WriteBatch分别写入到.log和MemTable中 

5) 最后通知生产者

二、写入.log文件

在上面已经提到数据会先写入到.log文件中,然后在写入MemTable中.leveldb中调用log::Writer::AddRecord方法:

/**
 * 添加记录
 * @param slice 记录内容 实际为string类型
 */
Status Writer::AddRecord(const Slice& slice) {
  const char* ptr = slice.data();
  size_t left = slice.size();

  // Fragment the record if necessary and emit it.  Note that if slice
  // is empty, we still want to iterate once to emit a single
  // zero-length record
  Status s;
  bool begin = true;
  do {
    const int leftover = kBlockSize - block_offset_;
    assert(leftover >= 0);
    if (leftover < kHeaderSize) {
      // Switch to a new block 可用空间小于7字节 则启用一个新的block 并且将剩余空间补0
      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));// PosixWritableFile类对象
      }
      block_offset_ = 0;
    }

    // Invariant: we never leave < kHeaderSize bytes in a block.
    assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
    //可用空间
    const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
    const size_t fragment_length = (left < avail) ? left : avail; //每次写入大小

    RecordType type;
    const bool end = (left == fragment_length);
    if (begin && end) {// 空间很充足 一次就够
      type = kFullType;
    } else if (begin) {//需要多次操作 这是一次
      type = kFirstType;
    } else if (end) {//需要多次操作 这是最后一次
      type = kLastType;
    } else {
      type = kMiddleType;//需要多次操作  这是中间操作
    }

    s = EmitPhysicalRecord(type, ptr, fragment_length);
    ptr += fragment_length;
    left -= fragment_length;
    begin = false;
  } while (s.ok() && left > 0);
  return s;
}

函数实现比较简单,注释也非常清晰。如果对log文件组织方式不清楚的,可参考本篇《leveldb深度剖析-存储结构》

三、总结

截止目前,整个插入流程还是算比较清晰简单,下一篇介绍数据如何插入到MemTable,这部分稍微复杂一点,但也不是特别复杂。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值