LevelDB/MemTable

用只插入的跳表组织 MemTable 版本记录

LevelDB 把用户键、序号和记录类型编码为内部键,放入跳表;同一用户键的较新版本排在前面,读取按快照序号定位。

bb74ef739索引与查找核验 2026-10-12
相关概念

工程问题#

写入缓冲区既要不断接收新记录,也要支持点查找和有序扫描。覆盖旧值会破坏已有快照,而给每次读写都加同一把锁又会增加争用。

实际实现#

MemTable::Add 在 Arena 中构造不可变记录,再调用 SkipList::Insert。内部键先按用户键升序、再按序号和类型降序比较。跳表随机升层,以高层链接跳过区间;插入先初始化新节点,再用 release store 发布前驱链接,读取用 acquire load。节点在整个 MemTable 存活期间不单独删除,因此读者不需要处理节点即时回收。

处理流程#

  1. 写入批次为记录分配序号,MemTable::Add 编码用户键、序号、类型和值;删除写入的是删除标记。
  2. FindGreaterOrEqual 从最高层前进并逐层下降,同时为插入记录各层前驱。
  3. 随机决定节点高度,先设置新节点的后继,再逐层发布前驱到新节点的链接。
  4. MemTable::Get 用含快照序号的 LookupKey 执行 Seek,跳过对该快照过新的版本,随后核对用户键并解释值或删除标记。
  5. MemTable 的引用归零后整体释放 Arena;快照与落盘文件的完整生命周期还由数据库上层维护。

源码片段#

固定版本 · 原文摘录
db/memtable.ccbb74ef739973a70ca9f0d90788a1da89b788c0ad · L84–L100
查看原文件
84  size_t key_size = key.size();85  size_t val_size = value.size();86  size_t internal_key_size = key_size + 8;87  const size_t encoded_len = VarintLength(internal_key_size) +88                             internal_key_size + VarintLength(val_size) +89                             val_size;90  char* buf = arena_.Allocate(encoded_len);91  char* p = EncodeVarint32(buf, internal_key_size);92  std::memcpy(p, key.data(), key_size);93  p += key_size;94  EncodeFixed64(p, (s << 8) | type);95  p += 8;96  p = EncodeVarint32(p, val_size);97  std::memcpy(p, value.data(), val_size);98  assert(p + val_size == buf + encoded_len);99  table_.Insert(buf);100}
MemTable::Add

先在 Arena 中编码记录,其中 tag 携带序号和记录类型;最后交给 table_.Insert。table_ 在 memtable.h 中定义为 SkipList<const char*, KeyComparator>。

db/dbformat.ccbb74ef739973a70ca9f0d90788a1da89b788c0ad · L47–L63
查看原文件
47int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {48  // Order by:49  //    increasing user key (according to user-supplied comparator)50  //    decreasing sequence number51  //    decreasing type (though sequence# should be enough to disambiguate)52  int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));53  if (r == 0) {54    const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);55    const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);56    if (anum > bnum) {57      r = -1;58    } else if (anum < bnum) {59      r = +1;60    }61  }62  return r;63}
InternalKeyComparator::Compare

先比较用户键,再反向比较序号与类型组成的 64 位 tag。这个顺序让 MemTable::Get 的 Seek 能跳过比快照更新的版本,而不是在跳表里原地覆盖旧值。

db/skiplist.hbb74ef739973a70ca9f0d90788a1da89b788c0ad · L259–L279
查看原文件
259typename SkipList<Key, Comparator>::Node*260SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,261                                              Node** prev) const {262  Node* x = head_;263  int level = GetMaxHeight() - 1;264  while (true) {265    Node* next = x->Next(level);266    if (KeyIsAfterNode(key, next)) {267      // Keep searching in this list268      x = next;269    } else {270      if (prev != nullptr) prev[level] = x;271      if (level == 0) {272        return next;273      } else {274        // Switch to next list275        level--;276      }277    }278  }279}
SkipList::FindGreaterOrEqual

在当前层向右跳过小于目标的节点;不能继续前进时记录前驱并下降一层。第 0 层返回第一个不小于目标的节点,查找与插入位置定位复用同一逻辑。

db/skiplist.hbb74ef739973a70ca9f0d90788a1da89b788c0ad · L149–L162
查看原文件
149  // Accessors/mutators for links.  Wrapped in methods so we can150  // add the appropriate barriers as necessary.151  Node* Next(int n) {152    assert(n >= 0);153    // Use an 'acquire load' so that we observe a fully initialized154    // version of the returned Node.155    return next_[n].load(std::memory_order_acquire);156  }157  void SetNext(int n, Node* x) {158    assert(n >= 0);159    // Use a 'release store' so that anybody who reads through this160    // pointer observes a fully initialized version of the inserted node.161    next_[n].store(x, std::memory_order_release);162  }
SkipList::Node::Next / SetNext

Next 的 acquire load 与发布链接的 release store 配合,使沿已发布链接读取的线程看到初始化完成的节点。原子链接不替代外部写入串行化与生命周期保护。

db/skiplist.hbb74ef739973a70ca9f0d90788a1da89b788c0ad · L359–L366
查看原文件
359  x = NewNode(key, height);360  for (int i = 0; i < height; i++) {361    // NoBarrier_SetNext() suffices since we will add a barrier when362    // we publish a pointer to "x" in prev[i].363    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));364    prev[i]->SetNext(i, x);365  }366}
SkipList::Insert

新节点先继承各层后继,再通过前驱的 SetNext 发布。低层先接入,即使读者暂时看不到新高层链接,也仍能沿低层继续查找。

db/skiplist.hbb74ef739973a70ca9f0d90788a1da89b788c0ad · L8–L26
查看原文件
8// Thread safety9// -------------10//11// Writes require external synchronization, most likely a mutex.12// Reads require a guarantee that the SkipList will not be destroyed13// while the read is in progress.  Apart from that, reads progress14// without any internal locking or synchronization.15//16// Invariants:17//18// (1) Allocated nodes are never deleted until the SkipList is19// destroyed.  This is trivially guaranteed by the code since we20// never delete any skip list nodes.21//22// (2) The contents of a Node except for the next/prev pointers are23// immutable after the Node has been linked into the SkipList.24// Only Insert() modifies the list, and it is careful to initialize25// a node and use release-stores to publish the nodes in one or26// more lists.
SkipList 并发约束

原文件明确要求写入外部同步、读者保护对象生命周期,且节点直到整个跳表销毁才删除。这些约束是简化读取的前提。

db/memtable.ccbb74ef739973a70ca9f0d90788a1da89b788c0ad · L102–L120
查看原文件
102bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {103  Slice memkey = key.memtable_key();104  Table::Iterator iter(&table_);105  iter.Seek(memkey.data());106  if (iter.Valid()) {107    // entry format is:108    //    klength  varint32109    //    userkey  char[klength]110    //    tag      uint64111    //    vlength  varint32112    //    value    char[vlength]113    // Check that it belongs to same user key.  We do not check the114    // sequence number since the Seek() call above should have skipped115    // all entries with overly large sequence numbers.116    const char* entry = iter.key();117    uint32_t key_length;118    const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);119    if (comparator_.comparator.user_comparator()->Compare(120            Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
MemTable::Get

用携带快照序号的内部查找键 Seek,跳过更新的版本,再核对是否属于同一个用户键。

db/memtable.ccbb74ef739973a70ca9f0d90788a1da89b788c0ad · L122–L135
查看原文件
122      const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);123      switch (static_cast<ValueType>(tag & 0xff)) {124        case kTypeValue: {125          Slice v = GetLengthPrefixedSlice(key_ptr + key_length);126          value->assign(v.data(), v.size());127          return true;128        }129        case kTypeDeletion:130          *s = Status::NotFound(Slice());131          return true;132      }133    }134  }135  return false;
MemTable::Get

用户键相同后按记录类型解释结果:返回保存的值,或将删除标记转换为 NotFound;不是同一个键则由调用方继续查找其他层。

各段均为固定提交连续原文。DBImpl::Write 的写入队列保证同一时刻只有获准批次修改 MemTable;不是在调用 Add 的整个期间一直持有数据库 mutex。

收益与取舍#

工程分析

带来的收益

  • 一套有序索引同时支持点查找、从指定键开始扫描与生成有序落盘数据。
  • 多版本作为不同内部键共存,更新不必原地改写读者正在访问的值。
  • 节点不单独删除,减少了并发读取时的回收协议;Arena 也减少逐节点分配与释放。

付出的代价

  • 只插入意味着旧版本和删除标记会继续占用当前 MemTable,空间回收依赖切换、落盘及后续清理。
  • 跳表查找和插入是概率意义上的高效,不能宣称最坏 O(log n);此实现最高 12 层,层高上限也限制了无限规模下的渐近推论。
  • 写入仍要求外部串行化;读者必须保证 MemTable 生命周期。单个跳表无需内部读锁,不等于整个 LevelDB API 无锁。

可比较的选择

平衡搜索树也能支持有序查询,并有确定的树高界;但旋转以及并发读者可见的结构修改需要另一套同步设计。

哈希表适合点查询,但有序扫描、按快照寻找同键版本和输出排序数据需要额外组织。

版本与适用范围#

固定提交 bb74ef739973 的 MemTable 跳表。层高每次以 1/4 的概率增加,最大为 12;复杂度还受键比较长度影响。快照可见性依赖内部键比较顺序和上层序号选择,不能仅凭原子 next 指针证明数据库事务语义。持久化、SSTable 查询与 compaction 不在本例展开。

参考来源#

源码、官方文档与原始论文
01
leveldb · MemTable::Add / MemTable::Get

db/memtable.cc · MemTable::Add / MemTable::Get

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad
02
leveldb · SkipList::Insert / FindGreaterOrEqual

db/skiplist.h · SkipList::Insert / FindGreaterOrEqual

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad
03
leveldb · InternalKeyComparator::Compare

db/dbformat.cc · InternalKeyComparator::Compare

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad
04
leveldb · DBImpl::Write

db/db_impl.cc · DBImpl::Write

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad
05
leveldb · MemTableInserter

db/write_batch.cc · MemTableInserter

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad
06
leveldb · MemTable::Table

db/memtable.h · MemTable::Table

源码bb74ef739973a70ca9f0d90788a1da89b788c0ad

相关实现与比较#

4 个相关案例
9 个匹配案例