kv数据库-leveldb (8) 内存表 (MemTable)

在上一章 预写日志 (Log / WAL) 中,我们了解了 LevelDB 如何通过"记流水账"的方式,确保任何写入操作在返回成功时都已持久化,即使发生断电也不会丢失。这解决了数据的安全问题。

现在,我们面临一个新的问题:日志是顺序追加的,查找效率很低。为了让用户能够快速地读到刚刚写入的数据,我们需要一个位于内存中的、支持快速查询的数据结构。这本"写字板"或"速记本",就是本章的主角------MemTable(内存表)。

什么是 MemTable

MemTable 是一个位于内存中的数据结构,用于缓冲新的写入操作。你可以把它想象成一块高效的写字板 。当你进行 PutDelete 操作时,在数据被记入 WAL 流水账之后,它会立刻被写到这块写字板上。

这块写字板有几个关键特性:

  1. 速度极快:因为它完全在内存中,写入操作几乎没有延迟。
  2. 内容有序:所有写在上面的键值对,都会自动按照键(key)的顺序排列好。
  3. 查询方便:因为内容有序,所以查找某个特定的键也同样迅速。

DB::Get 操作会优先查询这块写字板。由于大部分应用都倾向于读取最近写入的数据(数据局部性原理),MemTable 的存在极大地提升了数据库的读取性能。

当这块写字板写满时(大小由 选项 (Options) 中的 write_buffer_size 控制),LevelDB 会把它"冻结",变成一块只读的"历史记录板"(我们称之为不可变 MemTable),然后换上一块新的、干净的写字板继续工作。同时,会有一个后台线程默默地将这块"历史记录板"上的内容,整理成一个永久的文件存档,存入磁盘。这个文件存档就是我们下一章要讲的 排序字符串表 (SSTable)。

MemTable 的核心:跳表 (Skip List)

为了在内存中既能快速写入又能保持数据有序,MemTable 需要一个高效的底层数据结构。LevelDB 在这里选择了一个非常巧妙的数据结构------跳表 (Skip List)

什么是跳表?让我们用一个比喻来理解。

想象一下,你要在一长串排好队的珠子里找到第 50 颗。最笨的方法是从第 1 颗开始,一颗一颗地往后数。这就是普通的链表。

而跳表则是在这串珠子之上,加了好几层"快速通道"。

  • 第一层(最底层):连接了所有的珠子,就像普通链表。
  • 第二层:每隔几颗珠子,就拉一根"快进线",连接它们。
  • 第三层:在第二层的基础上,再隔更远的距离拉一根"超级快进线"。
graph TD subgraph 跳表示意图 direction LR subgraph "Level 2 (超级快进线)" L2_1(1) --> L2_9(9) end subgraph "Level 1 (快进线)" L1_1(1) --> L1_4(4) --> L1_7(7) --> L1_9(9) end subgraph "Level 0 (基础链表)" L0_H(Head) --> L0_1(1) --> L0_2(2) --> L0_3(3) --> L0_4(4) --> L0_5(5) --> L0_6(6) --> L0_7(7) --> L0_8(8) --> L0_9(9) end L2_1 --o L1_1 --o L0_1 L1_4 --o L0_4 L1_7 --o L0_7 L2_9 --o L1_9 --o L0_9 end

现在,你要找第 8 颗珠子。你可以:

  1. 先走"超级快进线",从 1 直接跳到 9。发现跳过了,退回到 1。
  2. 降到"快进线",从 1 跳到 4,再跳到 7,再跳到 9。又跳过了,退回到 7。
  3. 降到基础链表,从 7 往后走一步,就找到了 8。

通过这种方式,我们跳过了大量的中间节点,查找速度得到了指数级的提升。跳表的插入和查找性能与平衡二叉树相当,但实现起来更简单,也更适合并发场景。

MemTable 的工作流程

MemTable 是 LevelDB 内部自动管理的,我们无法直接操作它。但理解它的工作流程对理解 LevelDB 的性能至关重要。

写入操作 (Put)

当你调用 db->Put("key", "value") 时,MemTable 参与的流程如下:

sequenceDiagram participant App as 你的应用 participant DB as DB 实例 participant WAL as 预写日志 participant MemTable as 内存表 (活跃) App->>DB: Put("key", "value") DB->>WAL: 1. 写入日志 WAL-->>DB: 成功 DB->>MemTable: 2. Add("key", "value") Note right of MemTable: 使用跳表快速插入并保持有序 MemTable-->>DB: 成功 DB-->>App: 返回成功

读取操作 (Get)

当你调用 db->Get("key") 时,MemTable 是第一道被查询的关卡。

sequenceDiagram participant App as 你的应用 participant DB as DB 实例 participant MemTable as 内存表 (活跃) participant ImmutableMem as 内存表 (不可变) participant SSTables as 磁盘文件 App->>DB: Get("key") DB->>MemTable: 1. 先查活跃的 MemTable alt 在活跃 MemTable 中找到 MemTable-->>DB: 返回 value DB-->>App: 返回 value else 未找到 MemTable-->>DB: 未找到 DB->>ImmutableMem: 2. 再查不可变的 MemTable alt 在不可变 MemTable 中找到 ImmutableMem-->>DB: 返回 value DB-->>App: 返回 value else 未找到 ImmutableMem-->>DB: 未找到 DB->>SSTables: 3. 最后才查磁盘文件 SSTables-->>DB: ... DB-->>App: ... end end

这个查询顺序(活跃 MemTable -> 不可变 MemTable -> 磁盘文件)确保了最新的数据总能被最快地找到。

深入代码实现

让我们深入代码,看看 MemTable 和它的核心 SkipList 是如何实现的。

MemTable 类 (db/memtable.h)

MemTable 类的定义非常简洁,它封装了底层的跳表。

cpp 复制代码
// 来自 db/memtable.h (简化后)
class MemTable {
 public:
  explicit MemTable(const InternalKeyComparator& comparator);

  // 向 MemTable 中添加一条记录
  void Add(SequenceNumber seq, ValueType type, const Slice& key,
           const Slice& value);

  // 从 MemTable 中获取一条记录
  bool Get(const LookupKey& key, std::string* value, Status* s);
  
  // ... 其他方法 ...

 private:
  struct KeyComparator { /* ... */ };
  typedef SkipList<const char*, KeyComparator> Table;

  KeyComparator comparator_;
  Arena arena_; // 内存分配器
  Table table_; // 底层的跳表
};
  • table_: 这就是底层的跳表实例。它存储的键是 const char*,指向下面 arena_ 中分配的内存。
  • arena_: 这是一个自定义的内存分配器。为了避免频繁地调用 newdelete 带来的性能开销和内存碎片,MemTable 会先向系统申请一大块内存(Arena),然后跳表的所有节点都在这块内存上进行分配。当 MemTable 被销毁时,整块 Arena 内存会被一次性释放,效率非常高。

MemTable::Add 方法 (db/memtable.cc)

Add 方法负责将键、值、序列号和操作类型打包成一个连续的内存块,然后插入到跳表中。

cpp 复制代码
// 来自 db/memtable.cc (简化后)
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
                   const Slice& value) {
  // 计算内部键和值编码后的总长度
  const size_t encoded_len = ...;
  // 从 Arena 中分配所需内存
  char* buf = arena_.Allocate(encoded_len);
  
  // 将 internal_key 和 value 编码进 buf ...
  // ...
  
  // 将指向这段内存的指针插入到跳表中
  table_.Insert(buf);
}

这个方法的核心在于,它将所有信息(键、值、元数据)序列化到由 Arena 管理的一块内存中,然后只把指向这块内存的指针作为键插入到跳表中。这样做既高效又节省内存。

SkipList 类 (db/skiplist.h)

跳表的实现是 LevelDB 的一个亮点。我们来看一下它的节点定义和插入逻辑。

Node 结构

每个节点都包含一个键,以及一个指向不同层级下一个节点的指针数组。

cpp 复制代码
// 来自 db/skiplist.h (简化后)
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}
  Key const key; // 节点存储的键

  // 指向下一个节点的指针数组
  // next_[0] 是最底层的指针
  std::atomic<Node*> next_[1];
};

next_ 数组的大小是在创建节点时动态决定的,取决于这个节点拥有的"快进线"层数。

Insert 方法

插入新节点时,会随机生成一个高度(层数),这决定了新节点会出现在哪些"快进线"层上。

cpp 复制代码
// 来自 db/skiplist.h (简化后)
template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  // 1. 找到每一层应该插入的位置
  Node* prev[kMaxHeight];
  FindGreaterOrEqual(key, prev);

  // 2. 随机生成一个新节点的高度
  int height = RandomHeight();
  
  // 3. 创建新节点,并将它连接到每一层的链表中
  Node* x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    x->SetNext(i, prev[i]->Next(i));
    prev[i]->SetNext(i, x);
  }
}

这个 RandomHeight() 函数是跳表的精髓所在。它通过一个简单的概率算法,就能构造出一个高效的、多层次的索引结构,就像我们之前比喻的地铁快线系统一样。

总结

在本章中,我们深入了解了 LevelDB 在内存中的核心组件------MemTable

  • MemTable 是一个有序的、位于内存中的写缓冲区,它使得写入操作极快,并能加速对新数据的读取。
  • 它的底层实现是跳表 (Skip List),一个性能堪比平衡树但实现更简单的数据结构。
  • 为了提高内存分配效率,MemTable 使用了 Arena 来进行内存管理。
  • MemTable 写满后,它会变为不可变 (immutable) 状态,等待后台线程将其内容转存到磁盘。

我们已经清楚了数据在内存中的旅程:从写入 WAL 保证安全,到存入 MemTable 提供快速访问。但是,内存终究是有限的。当 MemTable 的数据需要被永久保存时,它们会去向何方?

相关推荐
了不起的杰2 小时前
【Redis】:从应用了解Redis
数据库·redis·缓存
翻斗花园刘大胆3 小时前
JavaWeb之HttpServletRequest与HttpServletResponse详解及快递管理系统实践
java·开发语言·数据库·mysql·servlet·架构·mvc
remaindertime3 小时前
从“万能 ES”到专业 ClickHouse:一次埋点数据存储的选择
数据库·架构
双向333 小时前
【征文计划】深度剖析 Rokid SLAM 算法:从传感器融合到空间重建的完整技术链路
数据库
Elastic 中国社区官方博客4 小时前
使用 TwelveLabs 的 Marengo 视频嵌入模型与 Amazon Bedrock 和 Elasticsearch
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
necessary6537 小时前
explain analyze和直接执行SQL时间相差10倍?
数据库
向宇it7 小时前
【Mysql知识】Mysql索引相关知识详解
数据库·mysql
文人sec8 小时前
性能测试-jmeter13-性能资源指标监控
数据库·测试工具·jmeter·性能优化·模块测试
摩羯座-1856903059410 小时前
VVIC 平台商品详情接口高效调用方案:从签名验证到数据解析全流程
java·前端·数据库·爬虫·python