LevelDB 浅谈4--多线程写操作

1,776 阅读2分钟

LevelDB 数据库是可以多线程操作的数据库。前几节中,我们讲解了批量插入数据的底层实现。这一节,我们重点讲解,LevelDB 对外开放的写操作,讲解 LevelDB 是如何协调多线程的插入操作。

讲解的顺序,就依照 key-value 在 LevelDB 的游走来讲解。

// Convenience methods
Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
  return DB::Put(o, key, val);
}

Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
  WriteBatch batch;
  std::cout<< "first step" << std::endl;
  // 现将key和value存储到 WriteBatch中的req_中
  batch.Put(key, value);
  return Write(opt, &batch);
}

其中 DB 是 BPImple 的父类, LevelDB 接收到 key-value 后,首先将 key-value 存储到 WriteBatch 中。WriteBatch 虽然是实现批量写的类,但这个时候,每个 WriteBatch 类中,只存储一个 key-value。

DBImpl::Write

下面重点讲解 LevelDB 如果协调多个线程的的操作,其中涉及了互斥锁和条件锁。

step1: 对 WriteBatch 进行封装。

  Writer w(&mutex_);
  w.batch = updates;  // 记录要写入的数据
  w.sync = options.sync;
  w.done = false;
  

Writer 类封装 WriteBatch,封装的目的,是为了维护一系列状态信息和锁(之后用于条件锁)。

step2:插入到任务列表,并等待再次被唤醒的条件

  MutexLock l(&mutex_);
  writers_.push_back(&w);
  while (!w.done && &w != writers_.front()) {
    w.cv.Wait(); 
  }
  if (w.done) {
    return w.status;
  }

writers_ 是任务队列。多个线程作为生产者,当其获得锁后,将 WriteBatch 任务插入到 writers_ 中。其中 &w != writers_.front() 时,该线程变身为消费者,去处理 任务列表中的任务。

上述代码涉及到条件锁,只有两种方式该线程才会再次被唤醒:

  1. 该线程处理的 WriterBatch 已经被其他线程处理;
  2. 该现成处理的 WriterBatch 排在了队列的第一位,并被其他线程唤醒。

当时第一种情况,代表该线程插入到 writers_ 中的 WriteBatch 已经被其他线程处理完成,所以直接 return。

第二种情况,就是该线程 变为消费者,去处理 writers_ 中的任务。

step3:检查MemTable是否又足够空间

  Status status = MakeRoomForWrite(updates == nullptr);
  uint64_t last_sequence = versions_->LastSequence();

函数 MakeRoomForWrite 之后重点讲解。

step4:合并 writers_ 中的任务,实施合并写操作

// 指向当前需要写入的数据
  Writer* last_writer = &w;
  if (status.ok() && updates != nullptr) {  // nullptr batch is for compactions
    WriteBatch* write_batch = BuildBatchGroup(&last_writer);
    WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);
    last_sequence += WriteBatchInternal::Count(write_batch);

    // 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();
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      if (status.ok()) {
        status = WriteBatchInternal::InsertInto(write_batch, 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 (write_batch == tmp_batch_) tmp_batch_->Clear();

    versions_->SetLastSequence(last_sequence);
  }

代码运行到这里,代表 MemTable 又足够的空间。这里主要涉及几个操作:

  1. 合并 WriteBatch。在函数 BuildBatchGroup 中实现;
  2. 调用 WriteBatchInternal::InsertInto,实现插入操作;
  3. 设置 last_sequence

step5:删除已处理任务,唤醒头部任务线程

  while (true) {
    // ready 即为刚出完成的线程
    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();
  }