[LevelDB]一文了解LevelDB数据库读取流程

85 阅读6分钟

@TOC

LevelDB 读取有以下特点

  1. 多层查找策略:使用memory 和多级别的sstable来进行分层缓存来进行查询
  2. 版本控制: 使用快照来保证查询操作数据一致性
  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");
    s = db->Put(write_opts, "time", "20240319");

    // 数据查询
    std::string value;
    leveldb::ReadOptions read_opts;
    s = db->Get(read_opts, "", &value);

2. 读取快照

  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();// 隐式转换
  } else {
    snapshot = versions_->LastSequence();
  }
}

读取快照,如果存在指定快照则读取指定快照,如果不存在就读取最新的快照 快照的目的是为了实现MVCC 主要有以下的特点

  1. 提供读取历史能力的能力
  2. 保证读取的一致性
  3. 支持多版本的并发控制

3. 初始化可变内存和不可变内存

// 代码地址:db/db_impl.cc
  MemTable* mem = mem_;// 可变内存
  MemTable* imm = imm_;// 不可变内存

这里的可变内存和不可变内存,是因为levelDB的LSM-Tree机制,简单来说,就是多级缓存来提升效率,数据在写入时会先写入可变内存,可变内存满了随后变成不可变内存,不可变内存会异步写入磁盘中的level 1层,多个level1会被压缩变成level2,以及往上递增,默认最多时7层

具体可以参考博客:XXXX

4. 核心逻辑:分级别读取数据

    mutex_.Unlock();// 解锁
    LookupKey lkey(key, snapshot);// 构建读取辅助类
    if (mem->Get(lkey, value, &s)) {// 从内存中读取锁
      // Done
    } else if (imm != nullptr && imm->Get(lkey, value, &s)) {// 查找不可变内存表
      // Done
    } else {
      s = current->Get(options, lkey, value, &stats);// 查找持久化存储
      have_stat_update = true;
    }
    mutex_.Lock();// 重新上锁

4.1 内存读取方法

  Slice memkey = key.memtable_key();
  Table::Iterator iter(&table_);
  iter.Seek(memkey.data()); // 使用跳表的seek操作读取对应的数据

这里对内存的检索做了优化 简单来说就是 使用多层索引组织了在内存中的形式, 表现是实现了一个跳表,相当于在kv这种使用有序key做排列的组织中,对key进行跳跃式的检索。

  Slice memkey = key.memtable_key();
  Table::Iterator iter(&table_);
  iter.Seek(memkey.data()); // 使用跳表的seek操作读取对应的数据

核心的内存检索逻辑

 // 从最高层开始
  int level = GetMaxHeight() - 1;
  while (true) {
    // 获取当前层的下一个节点
    Node* next = x->Next(level);
    // 如果当前层的下一个节点大于key, 则继续向下层查找
    if (KeyIsAfterNode(key, next)) {
      // Keep searching in this list
      x = next;
    } else {
      // 如果prev不为空, 则将当前节点赋值给prev[level]
      if (prev != nullptr) prev[level] = x;
      // 如果当前层为0层, 则返回下一个节点
      if (level == 0) {
        return next;
      } else {
        // 如果当前层不是0层, 则继续向下层查找  
        level--;
      }
    }
  }

举个简单的例子

// 跳表的金字塔结构
Level 3:  head ----------------> 20 -------------------------> 50
                                |                              |
Level 2:  head --------> 15 --> 20 -----------> 35 ---------> 50
                         |      |                |            |
Level 1:  head --> 10 -> 15 -> 20 --> 25 ----> 35 --> 45 -> 50
                  |     |      |       |         |      |     |
Level 0:  head -> 10 -> 15 -> 20 -> 25 -> 30 -> 35 -> 45 -> 50
// 查找值为27的节点
Level 3: head ------> 20 --------|  // 快速跳到20
                                 v
Level 2: head -> 15 -> 20 -----> 35 // 发现35太大,向下
                           v
Level 1: head -> ... -> 25 -> 35    // 找到25,继续
                        v
Level 0: head -> ... -> 25 -> 30    // 最终定位

具体可以参考博客: todo: XXXX

5. 在磁盘中sstable中进行检索

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

6. 写入数据到内存中

s = current->Get(options, lkey, value, &stats);// 查找持久化存储

简单来说 这个函数调用的检索是检索磁盘上的sstable,首先会读取磁盘上文件的缓存数据,如果没有就检索更下层的文件,读取操作系统中的文件

  // 创建索引块的迭代器 
  Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
  iiter->Seek(k);//在索引块中查找
  
  if (iiter->Valid()) {
    // 找到可能包含目标key的位置
    Slice handle_value = iiter->value();
    // 布隆过滤器检查 
    FilterBlockReader* filter = rep_->filter;
    BlockHandle handle;
    if (filter != nullptr && handle.DecodeFrom(&handle_value).ok() &&
        !filter->KeyMayMatch(handle.offset(), k)) {// 如果布隆过滤器表示key不存在,直接返回
      // Not found
    } else {
      // 读取实际的数据块
      Iterator* block_iter = BlockReader(this, options, iiter->value());
      // 在数据块中查找
      block_iter->Seek(k);
      if (block_iter->Valid()) {
        // 找到数据,调用回调函数处理结果
        (*handle_result)(arg, block_iter->key(), block_iter->value());
      }
      s = block_iter->status();
      delete block_iter;
    }
  }

7. 接下来在后台调用压缩层级的方法

MaybeScheduleCompaction(); // 压缩层级的方法 主要是为了提升读取的性能

通过环境变量 env_ 在后台调用压缩方法

mutex_.AssertHeld();// 确保 互斥锁已经持有:主要是为了确保能拿到 background_compaction_scheduled_ 
  if (background_compaction_scheduled_) {// 确保当前没有正在运行的压缩任务: 查看当前的后台压缩任务标识 是否为true, 如果为true
  } else if (shutting_down_.load(std::memory_order_acquire)) {// 确保数据库正常运行: 查看数据库, 是否已经关闭, 如果关闭, 不需要进行压缩
  } else if (!bg_error_.ok()) {// 检查是否有后台错误
  } else if (imm_ == nullptr && manual_compaction_ == nullptr &&
             !versions_->NeedsCompaction()) {// 判断是否需要压缩
    // 检查
    /**
     * 1. imm 是否为 nullptr, 表示没有不可变内存表需要处理
     * 2. manual_compaction_ 是否为 nullptr,表示没有手动压缩任务。
     * 3. versions_->NeedsCompaction() 是否返回 false,表示当前版本不需要压缩。
     */
  } else {
    /**
     * 后台运行 方法
     */
    background_compaction_scheduled_ = true;
    env_->Schedule(&DBImpl::BGWork, this);// 
  }

参考源码&项目链接

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

Status DBImpl::Get(const ReadOptions& options, const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  SequenceNumber snapshot;
  /**
   * 读取快照,如果存在指定快照则读取指定快照,如果不存在就读取最新的快照
   * 快照的目的是为了实现MVCC
   * 主要有以下的特点
   * 1. 提供读取历史能力的能力
   * 2. 保证读取的一致性
   * 3. 支持多版本的并发控制
   */
  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();// 隐式转换
  } else {
    snapshot = versions_->LastSequence();
  }
  
  MemTable* mem = mem_;// 可变内存
  MemTable* imm = imm_;// 不可变内存
  // 主要有两种 memtable
  Version* current = versions_->current();
  mem->Ref();// 增加引用
  // 
  if (imm != nullptr) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats; // 查看版本

  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();// 查找操作之前 释放互斥锁, 减少锁竞争, 因为查找过程中, 读取操作是无害的, 不需要持有锁
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) {// 从内存中读取锁
      // Done
    } else if (imm != nullptr && imm->Get(lkey, value, &s)) {// 查找不可变内存表
      // Done
    } else {
      s = current->Get(options, lkey, value, &stats);// 查找持久化存储
      have_stat_update = true;
    }
    mutex_.Lock();// 重新上锁
  }

  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction(); // 压缩层级的方法 主要是为了提升读取的性能
  }
  mem->Unref();                   
  if (imm != nullptr) imm->Unref();
  current->Unref();
  return s;
}

猜你喜欢

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

PS

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