leveldb源码学习2--Put操作--1)总体流程

Put操作的含义和接口定义

Put操作是levelDB对用户提供的一个接口,作用是把一个kv对写入到库中,定义为:

---------------------------------------------------------------------------------db_impl.cc
// Default implementations of convenience methods that subclasses of DB
// can call if they wish
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
  WriteBatch batch;
  batch.Put(key, value);
  return Write(opt, &batch);
}

这里面key和value参数不用解释了,第一个参数WriteOptions参看options.h中的定义发现就只有一个bool sync而已,默认为false,如果设置为true,则需要将数据从os的buffer中flush到文件之后write才会返回,但是这样的话一次写入操作会慢一些。

接口逻辑

levelDB按照批量写入,即使只是put一个kv对,也要放到一个代表“批量”的对象WriteBatch对象中再写入,因此先创建一个WriteBatch对象,然后这个WriteBatch对象调用自己的Put方法把单个kv对放进来,再继续以这个WriteBatch对象为参数继续调用levelDB的Write接口,这里看出levelDB的Put接口可以当成是levelDB的Write接口的一个马甲。

WriteBatch类及其Put操作

WriteBatch类定义在Write_batch.h中,其实就是拿一个string rep_以内存紧缩的方式存放一堆kv对,当然具体到这里Put接口里创建的batch就只需要存放单独的kv对了。

WriteBatch类有一个友元类WriteBatchInternal,这个WriteBatchInternal提供了一堆接口来操作WriteBatch,所有的接口都是静态ststic的,所以可以认为WriteBatchInternal是一个接口类,纯粹是提供一堆可以控制WriteBatch的私有成员的函数而已,为什么不直接定义在Writebatch里面呢,反正都是statsic的,感觉就是写代码方便啊。

WriteBatch类的Put操作用来把一个kv对放到WriteBatch对象里。

---------------------------------------------------------------------------write_batch.cc
void WriteBatch::Put(const Slice& key, const Slice& value) {
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  rep_.push_back(static_cast<char>(kTypeValue));
  PutLengthPrefixedSlice(&rep_, key);
  PutLengthPrefixedSlice(&rep_, value);
}

逻辑是这样的:

  1. 先自增当前batch里的kv对的数量;
  2. 然后把key类型放到rep_末尾,这里必然是kTypeValue,因为Put接口只针对非delete操作;
  3. 接着把key值和value值继续向rep_末尾添加。

就是这么单纯简单,唯一有点玄机的就是自增当前batch里kv对的数量,这个数量其实也是存放在rep_的rep_[8]~rep_[11]这四个字节里的(rep_[0]~rep_[7]的8个字节存放的是sequence)。

Write接口

WriteBatch准备好了,虽然这个Batch里其实只有一对kv值,直接用来当参数调用Write接口吧。

-----------------------------------------------------------------------------db_impl.cc
Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) {
  /*
   * 把WriteBatch放入到一个Writer结构中,这个结构除了记录需要写入的数据writebatch外,
   * 还记录了写入的状态等其他管理信息,
   */
  Writer w(&mutex_);
  w.batch = my_batch;
  w.sync = options.sync;
  w.done = false;

  /*
   * 将Writer放入到writers_中,writers_是一个双端队列std::deque,需要加锁,
   * 因为levelDB支持多线程,这个互斥锁MutexLock对象用来保护writers_结构
   * MutexLock是Mutexlock.h定义的一个类,其实就是执行mutex->lock()
   * 这个类对象在析构时会执行unlock*/
  MutexLock l(&mutex_);
  writers_.push_back(&w);
  /* 生产者线程把w放入deque后就开始在while里面睡眠,在两种情况下被唤醒:
   * 1.刚加入的这个任务被处理:done == true,退出循环直接返回
   * 2.加入的这个任务位于队列的头部,这个生产者线程变成消费者线程开始后面的处理
   */
  while (!w.done && &w != writers_.front()) {
    w.cv.Wait();
  }
  /* 被唤醒了一看,刚加入的w已经被done了,
   * 对应本函数末尾的【唤醒操作1】
   */
  if (w.done) {
    return w.status;
  }
  /* 之前加入的w处于队列头,开始消费吧,什么情况下被唤醒后发现是自己的任务处于队列头了呢,
   * 看本函数末尾有两个cv.Signal()中的第二个唤醒队列头Writer的操作【唤醒操作2】,貌似现在只有末尾那里有唤醒队列头
   */
  // May temporarily unlock and wait.
  /* 检查内存中的Memtable是否还有足够空间
   */
  Status status = MakeRoomForWrite(my_batch == nullptr);
  //last_sequence记录的是leveldb中已经写入的数据的最大序列号
  uint64_t last_sequence = versions_->LastSequence();
  //last_writer是自己的Writer,现在处于队列头
  Writer* last_writer = &w;
  //成功的发现Memtable还有空间
  if (status.ok() && my_batch != nullptr) {  // nullptr batch is for compactions
    /*
     * 将生产者队列中的所有任务组合成一个大的任务,综合这里的场景,就是将所有任务中的writebatch组合在一起
     * 形成一个包含所有wrirtebatch的kv的大的writebatch--updates,这里BuildBatchGroup会遍历当前wrirters_中的
     * 所有Writer,并将他们合并
     */
    WriteBatch* updates = BuildBatchGroup(&last_writer);
	/*
	 * 结合这里,我们就可以解释一直以来的sequence number(序列号)的具体含义了。
	 * 之前说过,Count函数返回writebatch中的key-value对数,因此sequence number记录的就是当前加入leveldb中的键值对个数,
	 * 每个键值对都会对应一个序列号,而且是独一无二的。last_sequence一如既往,记录当前的最大序列号。
	 */
    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();
	 /*
      *【step1】写入log文件
      */
	  //写log日志,这个东西是硬盘里的,以posix实现为例,最终会有write()系统调用
	  // Contents()其实就是Slice(batch->rep_),这里updates是一个大WriteBatch
      status = log_->AddRecord(WriteBatchInternal::Contents(updates));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
	  /* 
	   * 向内存中的MemTable添加数据了。这个函数把updates里面的所有K-V添加到Memtable中,
	   * sequence number也会融合在key里面。
	   * 这个地方是不加锁的,因此虽然InsertInto可能会执行较长时间,但是它也不会影响其他生产者线程向队列中添加任务
	   */
	  /*
       *【step2】写入mem_文件
       */
      if (status.ok()) {
        status = WriteBatchInternal::InsertInto(updates, mem_);
      }
	  /*
	   * 错误处理,以及last-sequence的处理
	   */
      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);
      }
    }
	//搞完了,把tmp_batch_里的内容全部清除掉,以备下次继续使用
	//说白了就是rep_.clear();rep_.resize(kHeader);
    if (updates == tmp_batch_) tmp_batch_->Clear();

    versions_->SetLastSequence(last_sequence);
  }
/*
 * 这部分代码是呼应最开始时的while循环里面的条件变量等待。
 * 前面虽然该消费者线程已经将任务都处理完了(添加到Memtable中)。但是任务并没有从队列中删除,
 * 这个while循环就是将已经处理的任务从队列中移除的过程,
 * 同时还会通知相应任务的生产者线程说明它所添加的任务已经处理完毕了(通过设置writer.done标记位),
 * 可以直接返回了,结合前面的while循环看一下还是很简单的。
 */
  while (true) {
    Writer* ready = writers_.front();
    writers_.pop_front();
    if (ready != &w) {
      ready->status = status;
      ready->done = true;
	  //【唤醒操作1】:这里将本次消费的所有Writer都唤醒
      ready->cv.Signal();
    }
    if (ready == last_writer) break;
  }

  // Notify new head of write queue
  // 最后这行代码也是和前面的while等待相呼应。它会唤醒在队列头等待的生产者线程,这个被唤醒的线程会充当下一轮的消费者。
  if (!writers_.empty()) {
  	//【唤醒操作2】:将队列头还没有消费的Writer唤醒
    writers_.front()->cv.Signal();
  }

  return status;
}

这个Write接口在levelDB的代码里算是行数比较大的了。

  1. 把WriteBatch对象又放在一个Writer结构里,Writer结构就定义在db_impl.cc开头,包含了要写的batch和几个控制状态的变量以及锁和条件变量;
  2. 把writer对象放入到writers_这个双端队列中,此时需要加锁,因为是多线程的,这个双端队列writers_需要锁保护;
  3. 然后本线程开始睡眠吧,等着被人唤醒,只有两种情况被唤醒,1)自己刚加入的writer位于双端队列头;2)自己刚加入的writer被其他线程处理了;
  4. 醒过来,发现是2)自己刚加入的writer被其他线程处理了,大功告成了直接返回,返回也就代表着给双端队列释放锁;否则继续;
  5. 既然不是2),那就是1)自己刚加入的writer位于双端队列头;所以需要开工了,先MakeRoomForWrite()看下内存中的Memtable是否还有空间,这个MakeRoomForWrite()非常复杂以后再将,总之有可能对双端队列解锁睡眠,在这期间双端队列里可能又被其他线程加入了更多的Writer对象;
  6. MakeRoomForWrite()返回,上面说到在这期间双端队列里可能又被其他线程加入了更多的Writer对象,先用BuildBatchGroup()把可能存在的这些更多的Writer对象和自己的Writer对象合并到一个新的WriteBatch对象updates中,这个新的WriteBatch对象中此时就真的包含若干个kv对了,这才像真的批量写入嘛;
  7. 对双端队列解锁,没必要锁了,等别人加把,反正新的WriteBatch updates拿到了开始真的写入了,先写硬盘的log文件,使用AddRecord()接口,一开始的sync选项起作用了,为true的话需要写透才返回;
  8. WriteBatchInternal::InsertInto()再写内存的memtable文件;
  9. 假设全都成功,此次写入可能写入了多个kv,也可能只写了一个,不管怎么样,把对应这些kv的Writer都从双端队列中pop出来,别忘了先给双端队列加锁;
  10. 所有写完的Writer都从双端队列里拿出来了,现在双端队列为空吗?不为空的话唤醒队列头的那个Writer,让那个线程继续当消费者吧;
  11. 返回,隐含给双端队列解锁。

猜你喜欢

转载自blog.csdn.net/haolianglh/article/details/82560987