[LevelDB]一文了解LevelDB数据库写入流程

92 阅读4分钟

@TOC

LevelDB 写入有以下特点

  1. 单进程多线程:即多个用户同时调用写入接口,将会将写入操作添加队列。
  2. WAL:日志预写入机制。
  3. 版本管理: 使用版本管理来保证写入原子性。

写入过程详解

1. 调用LevelDB的写入接口

	leveldb::DB *dbptr = nullptr; 
	// 创建数据库
	leveldb::Status s = leveldb::DB::Open(opts, "/tmp/leveldb_test01", &dbptr);
	// 调用写入接口
	s = db->Put(write_opts, "name", "leveldb");

2. 写入操作进入等待队列

	// 代码地址:db/db_impl.cc
	writers_.push_back(&w);// 将写入操作添加到writers(双端队列:deque:C)
	while (!w.done && &w != writers_.front()) {
	w.cv.Wait();// 如果当前的w 不是队列中第一个,并且没有完成就继续
}

3. 为写入操作腾空间

// 代码地址:db/db_impl.cc
	Status status = MakeRoomForWrite(updates == nullptr);// 为写入操作腾空间

MakeRoomForWrite 功能

  1. 判断当前是否满足压缩条件,后台无错误&内存满足限制
  2. 接下来 解决出现Level0文件数量不超过上限&&后台进程完成压缩操作的一些问题, 如果这些条件都不满足就需要自动触发切换日志文件,版本文件的操作,这样给文件写入留下充足的空间。
// 创建日志文件 
	s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
	// 创建版本管理文件 
	uint64_t new_log_number = versions_->NewFileNumber();
	versions_->ReuseFileNumber(new_log_number);

4. 切换版本

    WriteBatch* write_batch = BuildBatchGroup(&last_writer);// 构建写批处理组
    WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);// 更新序列号
    last_sequence += WriteBatchInternal::Count(write_batch); // 更新序列号

5. WAL 预写入日志操作

 mutex_.Unlock(); // 先拿锁
 status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));// 调用日志文件 写入 信息
 // 可选择的同步操作
 status = logfile_->Sync(); // 将内存中日志数据刷写到磁盘中

6. 写入数据到内存中

status = WriteBatchInternal::InsertInto(write_batch, mem_);// 然后把对应的数据 写入到内存中, 做缓存

7. 如果日志文件同步失败,后台记录这个错误

 RecordBackgroundError(status);// 记录背景错误

8. 当前线程成功写入后,使用cv通知其他线程

ready->cv.Signal();

参考源码&项目链接

github 链接: git@github.com:luogaiyu/leveldb.git

Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
  Writer w(&mute x_);   // mutex 主要用于共享资源占有的标记
  // 对写入操作进行标记
  w.batch = updates;
  w.sync = options.sync; 
  w.done = false;     

  MutexLock l(&mutex_); // 对当前操作进行上锁
  // 将当前的写入操作加入 writer: 双端队列
  writers_.push_back(&w);
  /**
   * 其实是判断当前的写入操作 是否完成
   * 当前写入操作是否排的是第一位
   * 如果都不是 就加入双端队列中等待
   */
  while (!w.done && &w != writers_.front()) {
    w.cv.Wait();// 如果当前的操作不满足要求, 就将队列加入 双端队列 等待
  }
  if (w.done) {
    return w.status;// 如果已经完成返回状态
  }
  // 有可能 短暂解锁并等待
  Status status = MakeRoomForWrite(updates == nullptr);// 为写操作 腾出压缩, 具体的原因是为了防止 level0的文件过多
  
  uint64_t last_sequence = versions_->LastSequence();// 使用版本控制获取序列号, 更新版本号的操作,1. 写入操作导致更新,2. 压缩操作导致更新
  Writer* last_writer = &w;

  if (status.ok() && updates != nullptr) {  // 如果扩容操作成功, 并且 批处理为空 说明当前是合并操作 compaction
//  -- 版本管理---
    WriteBatch* write_batch = BuildBatchGroup(&last_writer);// 构建写批处理组
    WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);// 更新序列号
    last_sequence += WriteBatchInternal::Count(write_batch); // 更新序列号
//  -- 版本管理---
    {
      mutex_.Unlock();
      // -- 向日志文件中新增数据--
      // log 指的是 write
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));// 调用日志文件 写入 write_batch的rep_[实际的数据]
      // -- 向日志文件中新增数据--
      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) { 
        /**
         * @brief 因为日志文件的状态不确定
         * 强制所有的写入操作都失败
         */
        RecordBackgroundError(status);// 记录背景错误
      }
    }
    if (write_batch == tmp_batch_) tmp_batch_->Clear();

    versions_->SetLastSequence(last_sequence);
  }
  // 写请求|担心q
  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;
  }
  
  // 通知新的写请求队列的头部
  if (!writers_.empty()) {
    writers_.front()->cv.Signal();
  }

  return status;
}

猜你喜欢

C++多线程: blog.csdn.net/luog_aiyu/a…

PS

你的赞是我很大的鼓励 欢迎大家加我飞书扩列, 希望能认识一些新朋友~ 二维码见: www.cnblogs.com/DarkChink/p…