技术贴 | Rocksdb 中 Memtable 源码解析

news2024/11/19 2:46:02

一、什么是 Memtable?

Memtable 是 Rocksdb 在内存中保存数据的一种数据结构,一个 Memtable 的容量是固定的,在 Memtable 写满后,会转换为  Immutable Memtable,Immutable Memtable 中的数据会 Flush 到 SST File 中。

Memtable 和 Immutable Memtable 的唯一区别是 Memtable 可读可写,而 Immutable Memtable 是只读且不允许写入。Rocksdb 引入了 Column Family 的概念,在一个 Column Family 中只有一个 Memtable,但允许存在多个 Immutable Memtable。Rocksdb 支持创建多数据结构类型的 Memtable,默认的是 SkipList,即跳跃表。

二、Memtable 的数据结构

Rocksdb 中 Memtable 有多种实现方式 (SkipList / HashSkipList / HashLinkList / Vector),其中默认的实现方式为 SkipList。

一个 Memtable 中维护了两个 SkipList,其中范围删除插入 range_del_table_,其余的操作写入 table_。

Memtable 定义的操作接口 Add () 如下:


bool MemTable::Add(SequenceNumber s, ValueType type,
                   const Slice& key, /* user key */
                   const Slice& value, bool allow_concurrent,
                   MemTablePostProcessInfo* post_process_info) {
  // 一条key-value Entry的数据格式
  //  key_size     : varint32 of internal_key.size()
  //  key bytes    : char[internal_key.size()]
  //  value_size   : varint32 of value.size()
  //  value bytes  : char[value.size()]
  uint32_t key_size = static_cast<uint32_t>(key.size());
  uint32_t val_size = static_cast<uint32_t>(value.size());
  uint32_t internal_key_size = key_size + 8;
  const uint32_t encoded_len = VarintLength(internal_key_size) +
                               internal_key_size + VarintLength(val_size) +
                               val_size;
  char* buf = nullptr;
  // 通过判断key-value的类型来选择memtable, 范围删除的kv插入range_del_table_
  std::unique_ptr<MemTableRep>& table =
      type == kTypeRangeDeletion ? range_del_table_ : table_;
  KeyHandle handle = table->Allocate(encoded_len, &buf);
  //...
  // 是否允许并发插入
  if (!allow_concurrent) {
    // 是否制定了函数提取key的前缀
    if (insert_with_hint_prefix_extractor_ != nullptr &&
        insert_with_hint_prefix_extractor_->InDomain(key_slice)) {
      // ...
      bool res = table->InsertWithHint(handle, &insert_hints_[prefix]);
    } else {
      // 插入key-value pair
      bool res = table->Insert(handle);
      if (UNLIKELY(!res)) {
        return res;
      }
    }
  } else {
    // 插入key-value pair
bool res = table->InsertConcurrently(handle);
    if (UNLIKELY(!res)) {
      return res;
    }
  }
  return true;
}

Add () 函数将用户的 key 和 value 封装成一个 buf,然后根据不同的条件调用 table->Insert () 插入至 Memtable。table 就是 Memtable 的工厂类实现,默认 SkiplistRep, 即通过调用 SkipList 的 Insert () 完成 key 的插入。

Memtable 定义的操作接口  Get () 如下:


bool MemTable::Get(const LookupKey& key, std::string* value, Status* s,
                   MergeContext* merge_context,
                   RangeDelAggregator* range_del_agg, SequenceNumber* seq,
                   const ReadOptions& read_opts, ReadCallback* callback,
                   bool* is_blob_index) {
  // 在range_del_table_上初始化一个迭代器
  std::unique_ptr<InternalIterator> range_del_iter(
      NewRangeTombstoneIterator(read_opts));
  Status status = range_del_agg->AddTombstones(std::move(range_del_iter));
  if (!status.ok()) {
    *s = status;
    return false;
  }

  Slice user_key = key.user_key();
  // 利用前缀提取过滤判断key是否存在
  bool const may_contain =
      nullptr == prefix_bloom_

Memtable 的 Get () 调用了 SkipListRep 的 Get () 接口,最终是通过 SkipList 的 FindGreaterOrEqual () 来查找。查找出来的 key 会被传入的回调函数 SaveValu 并 e () 根据 type 处理,例如 ktypeDeletion 就返回 NotFound ()。

三、什么是 SkipList?

SkipList 即跳跃表,在普通单向链表的基础上增加了一些索引,而且这些索引是分层的,从而可以快速地查到数据。如下是一个典型的跳跃表构建过程:

初始我们有个带头结点的有序链表 a,而后每相邻两个节点增加一个指针,让指针指向下下个节点,得到表 b。这样所有新增指针连成了一个新的链表,但它包含的节点个数只有原来的一半。其后我们对第二层链表再次进行此操作,得到表 c。重复这个过程,直到采样出的节点只剩一个,如图 d。这样便完成了跳跃表的构建。跳跃表查找过程如下:

从 head 开始,head 的 level 为 4,判断 head 后继节点值小于 <12,此时当前节点变为 6,继续查找;节点 6 的 level 为 3,判断后继节点值为 NIL,因此 level 降低到 2;判断 x -> forward [2] -> key (25) > 17,继续降低 level 到 1;判断 x -> forward [1] -> key (9) < 17,此时 x 变为 x ->forward [1],x 成为节点 9;节点 9 的判断 x -> forward [1] -> key 为 17,因此找到节点,直接返回。

跳跃表插入过程如下:

我们以上图为例,list -> leve=4,如果要插入节点 17,首先确定搜索路径,与之前步骤类似。

创建新节点 Node (17),并为其生成 level (随机),该 level 可能值为 [1, MaxLevel],此时需要对比,如果 level < list -> level,需要先将突出部分从 header 指向它,这里新生成的节点 Node (17) 的 level  为 5,超过了 list 当前的最大 level,于是将 update [4] 设置为 header,后续直接将 Node (17) 作为 header 的后继。

最后是设置搜索路径上每个节点的后继关系,这样我们便完成了节点的插入。我们来看一下 SkipList 的具体代码实现:

InlineSkipList 数据结构 >>


class InlineSkipList {
 private:
  struct Node;
  struct Splice;

 public:
  using DecodedKey = \
    typename std::remove_reference<Comparator>::type::DecodedType;
…
  Allocator* const allocator_; 
  Comparator const compare_;
  Node* const head_;

  std::atomic<int> max_height_;  

  Splice* seq_splice_;
};

Node 的数据结构 >>


template <class Comparator>
struct InlineSkipList<Comparator>::Node {
 
 private:
  // 存放该节点的next_节点的数组
  // 数组大小为该节点的height,当调用NewNode()分配内存初始化整个数组
  std::atomic<Node*> next_[1];
};

Node 的数据结构如图,它将 key 和链表每层的指针连续存储,通过 next_[-n] 这种方式来访问每层的 next 指针,此外在 new 新节点时会把该节点高度写在 next_[0] 的前 4 个字节处,当完成插入后,next_[0] 会恢复成指向同层的下一个节点的指针。

四、InlineSkipList 插入

Memtable 的 Add () 通过 SkipList 的 Insert () 来查找,下面是 Insert () 的具体实现:


bool InlineSkipList<Comparator>::Insert(const char* key, Splice* splice,
                                        bool allow_partial_splice_fix) {
  Node* x = reinterpret_cast<Node*>(const_cast<char*>(key)) - 1; // x即为next_[0]
  const DecodedKey key_decoded = compare_.decode_key(key);
  int height = x->UnstashHeight();
  assert(height >= 1 && height <= kMaxHeight_);
  int max_height = max_height_.load(std::memory_order_relaxed);
  // 更新max_height
  while (height > max_height) {
    if (max_height_.compare_exchange_weak(max_height, height)) {
      // successfully updated it
      max_height = height;
      break;
    }
    // 否则重试,可能因为其他人增加了它而退出循环
  }
  assert(max_height <= kMaxPossibleHeight);

  // 插入节点的时候,需要借助一个Splice对象,该对象主要保存着最近一次插入的节点快照
  // 它保存着一个prev和next的节点指针数组,由Level可以索引到对应Level的节点
  int recompute_height = 0;
  if (splice->height_ < max_height) {
    // 当重置splice
    splice->prev_[max_height] = head_;
    splice->next_[max_height] = nullptr;
    splice->height_ = max_height;
    recompute_height = max_height;
  } else {
    while (recompute_height < max_height) {
      if (splice->prev_[recompute_height]->Next(recompute_height) !=
          splice->next_[recompute_height]) { //判断该层的splice是否紧密,即prev_->Next是否等于next_
        ++recompute_height;
      } else if (splice->prev_[recompute_height] != head_ &&
                 !KeyIsAfterNode(key_decoded,
                                 splice->prev_[recompute_height])) { //小于splice当前层的prev_
        // ...
      } else if (KeyIsAfterNode(key_decoded,
                                splice->next_[recompute_height])) { //大于splice当前层的prev_
        // ...
      } else {
        // 找到了合适的level
        break;
      }
    }
  }
  assert(recompute_height <= max_height);
  if (recompute_height > 0) {//计算splice
    RecomputeSpliceLevels(key_decoded, splice, recompute_height); // 找到要插入的key合适的splice
  }

  bool splice_is_valid = true;
  if (UseCAS) {//CAS无锁机制
    //...
  } else {
    for (int i = 0; i < height; ++i) {
      if (i >= recompute_height &&
          splice->prev_[i]->Next(i) != splice->next_[i]) { // 确保splice此Level有效,如果无效的话再查找一次
        FindSpliceForLevel<false>(key_decoded, splice->prev_[i], nullptr, i,
                                  &splice->prev_[i], &splice->next_[i]);
      }
      // Checking for duplicate keys on the level 0 is sufficient
      if (UNLIKELY(i == 0 && splice->next_[i] != nullptr &&
                   compare_(x->Key(), splice->next_[i]->Key()) >= 0)) {
        // duplicate key
        return false;
      }
      if (UNLIKELY(i == 0 && splice->prev_[i] != head_ &&
                   compare_(splice->prev_[i]->Key(), x->Key()) >= 0)) {
        // duplicate key
        return false;
      }
      //…
      x->NoBarrier_SetNext(i, splice->next_[i]); //将新节点next指向对应的next节点
      splice->prev_[i]->SetNext(i, x); //将splice的prev节点的next指向新节点
    }
  }
  if (splice_is_valid) {//将新节点Height下的prev节点都设置为该节点,因为原先的prev和next之间已经不连续了。
    for (int i = 0; i < height; ++i) {
      splice->prev_[i] = x;
    }
    //...
  } else {
    splice->height_ = 0;
  }
  return true;
}

五、InlineSkipList 查找

Memtable 的 Get () 通过 SkipList 的 FindGreaterOrEqual () 来查找,下面是 FindGreaterOrEqual () 的具体实现:


InlineSkipList<Comparator>::FindGreaterOrEqual(const char* key) const {

  Node* x = head_;
  int level = GetMaxHeight() - 1;//从最高层开始查找
  Node* last_bigger = nullptr;
  const DecodedKey key_decoded = compare_.decode_key(key);
  while (true) {
    Node* next = x->Next(level); 
    if (next != nullptr) {
      PREFETCH(next->Next(level), 0, 1);
    }
    // Make sure the lists are sorted
    assert(x == head_ || next == nullptr || KeyIsAfterNode(next->Key(), x));
    // Make sure we haven't overshot during our search
    assert(x == head_ || KeyIsAfterNode(key_decoded, x));
    int cmp = (next == nullptr || next == last_bigger)
                  ? 1
                  : compare_(next->Key(), key_decoded);
    if (cmp == 0 || (cmp > 0 && level == 0)) { // 找到相等的key或者查找的key不在此范围内
      return next;
    } else if (cmp < 0) { //待查找 key 比 next 大,则在该层继续查找
      x = next;
    } else { // 待查找 key 不大于 next,且没到底,则继续往下层查找
      // Switch to next list, reuse compare_() result
      last_bigger = next;
      level--;
    }
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/11257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编程中老生常谈的【编码规范】你还记得多少?进来回顾一下吧【文末送书】

&#x1f3ac; 博客主页&#xff1a;https://xiaoy.blog.csdn.net &#x1f3a5; 本文由 呆呆敲代码的小Y 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;Unity精品学习专栏 &#x1f332; 游戏制作专栏推荐&#xff1a;游戏制作分享 &…

【genius_platform软件平台开发】第八十一讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)

1. ARM Neon Intrinsics 编程 1.入门&#xff1a;基本能上手写Intrinsics 1.1 Neon介绍、简明案例与编程惯例 1.2 如何检索Intrinsics 1.3 优化效果案例 1.4 如何在Android应用Neon 2. 进阶&#xff1a;注意细节处理&#xff0c;学习常用算子的实现 2.1 与Neon相关的ARM体系结…

寻 友 软 件

寻友软件项目技术技术功能部署Redis部署RocketMQJWT&#xff08;Json Web Token&#xff09;虹软人脸识别部署MongoDB&#xff08;尽量不用docker部署mongo&#xff09;部署Nginx过滤器及拦截器加缓存编码流程DOC接口文档bug技术 技术 前端&#xff1a; flutterandroid环信S…

分销微信小程序介绍_分销小程序有什么作用呢

不同的微商城系统对于分销功能的支持会有不要的叫法&#xff0c;一般来说主要有两种&#xff0c;一种是基于商品分享的分销方式&#xff0c;通过分享链接识别客户从属关系&#xff0c;订单完成&#xff0c;结算佣金&#xff1b;另一种分销商可以建立并独立运营一个分销店铺&…

【JavaSE】关于多态那些事儿

目录 1. 多态 1.1 多态的概念 1.2 多态实现条件 1.3 向上转型 1.3.1 直接赋值 1.3.2 方法传参 1.3.3 方法返回 1.3.4 向上转型的优缺点 1.4 重写 1.4.1 重写的条件 1.4.2 重写注意事项 1.4.3 重载与重写的区别 1.5 通过父类的引用&#xff0c;调用这个父类和子类重…

CSS篇十六——盒子模型之边框

目录一、CSS盒子模型1.1 盒子模型组成1.2 边框&#xff08;border&#xff09;1.2.1 语法格式1.2.2 边框样式 border-style1.2.3 代码示例1.3 表格的细线边框1.3.1 语法格式、代码示例及结果一、CSS盒子模型 网页布局过程&#xff1a; 1.先准备好相关的网页元素&#xff0c;网…

My sql的深度剖析

一.数据库的创建、删除、使用 数据库的创建&#xff1a;create database 数据库名 数据库的删除&#xff1a;drop database 数据库名&#xff1b; 数据库的使用&#xff1a;use数据名&#xff1b; 所有数据库的查看&#xff1a;show databases; 建立数据时如何指定字符集…

在Java中计算Levenshtein莱文斯坦(相似度)编辑距离

在本教程中&#xff0c;我们将研究 Levenshtein 距离算法&#xff0c;该算法也称为编辑距离算法&#xff0c;用于比较单词的相似性。 什么是列文施泰因距离 Levenshtein距离算法由俄罗斯科学家Vladimir Levenshtein创建。 Levenshtein 距离算法通过计算将一个字符串转换为另…

基于单片机的贪吃蛇设计

1 绪论 1.1 设计目的 在21世纪的今天&#xff0c;人们的生活开始变得更加丰富多彩。在繁忙的工作之余&#xff0c;娱乐成为人们生活不可或缺的一份子&#xff0c;而游戏作为近年来逐渐兴起的一种娱乐方式&#xff0c;已经越来越受到人们的青睐。在工作学习之余&#…

dreamweaver网页设计作业制作 学生NBA篮球网页 WEB静态网页作业模板 大学生校园篮球网页代码 dw个人网页作业成品

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

第五章:双指针与离散化的映射

第五章&#xff1a;双指针、离散化、二进制运算与区间合并一、双指针1、什么是双指针&#xff1f;2、双指针的模板3、双指针例题&#xff08;1&#xff09;思路&#xff1a;&#xff08;2&#xff09;解答&#xff1a;C版&#xff1a;C版&#xff1a;二、离散化1、什么是离散化…

java面试强基(3)

重载和重写的区别? 重载 发生在同一个类中&#xff0c;方法名必须相同&#xff0c;参数类型不同、个数不同、顺序不同&#xff0c;方法返回值和访问修饰符可以不同。 重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 重写 重写发生在运行期&#xff0c;…

go语言基本环境搭建

下载地址 Go官网下载地址&#xff1a;https://studygolang.com/dl 一、下载对应电脑得安装包 二、下载完成点击安装下一步&#xff08;选择目录尽量简单&#xff09; 三、是否安装成功 四、环境变量 GOROOT和GOPATH都是环境变量&#xff0c;其中GOROOT是我们安装go开发包的路…

【计算机毕业设计】Springboot医疗管理系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 医疗服务系统&#xff0c;主要的模块包括查看管理员&#xff1b;首页、个人中心…

tomcat官网下载配置全部详细步骤(包含各种报错解决办法)

前言&#xff1a; 名字由来&#xff1a;翻译是野猫&#xff0c;tomcat的作者初衷是希望这个软件可以自力更生&#xff0c;自给自足。不依赖其他插件&#xff0c;独立达到提供web服务的效果 1.tocat和java的关系&#xff1f; tomcat是用Java语言编写的&#xff0c;需要运行在…

大三,请问现在自学Java还来得及吗?

前言 如果还在为入门Java晚而发愁时间够不够&#xff0c;首先你是准备自学&#xff0c;那么我们可以看看现在网络上一些比较热门的Java全体系的学习需要化多长时间&#xff0c;先拿B站上做的比较好的黑马教程和尚硅谷举例&#xff1a; 2022黑马程序员Java学习路线图​www.bili…

耗时半月,终于把牛客网软件测试面试八股文,整理成了文档资料.....

一、面试基础题 简述测试流程: 1、阅读相关技术文档&#xff08;如产品PRD、UI设计、产品流程图等&#xff09;。 2、参加需求评审会议。 3、根据最终确定的需求文档编写测试计划。 4、编写测试用例&#xff08;等价类划分法、边界值分析法等&#xff09;。 5、用例评审(…

飞象星球落地重庆云阳86所学校,县乡4万学生迎来素质课堂

猜生字笔画顺序、学习硬笔书法&#xff1b;跟随老师认识情绪、写下心里话……自从重庆云阳县86所中小学引入飞象星球双师素质课堂&#xff0c;4万多名县城和乡村孩子的课后素质课堂一下子变得丰富多彩起来。 图&#xff1a;洞鹿小学双河村校上双师素质书法课 云阳县地处三峡库…

代码随想录算法训练营第三十六天| LeetCode435. 无重叠区间、LeetCode763. 划分字母区间、LeetCode56. 合并区间

一、LeetCode435. 无重叠区间 1&#xff1a;题目描述&#xff08;435. 无重叠区间&#xff09; 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。 2&#xff1a;解题思路 class …

MySQL面试问题汇总(2022)

一、MySQL架构 锁 什么是锁&#xff1f; 当多个连接并发地存取MySQL数据时&#xff0c;在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据&#xff0c;破坏数据库的一致性。 加锁是实现数据库并发控制的一个非常重要的…