RocksDB源码学习:MemTable实现之SkipList
源码解读关于RocksDB中MemTable的SkipList实现,Generated by AI。
前言
本文主要是 RocksDB 的 MemTable
核心实现的学习笔记。我们将从 memtable/skiplistrep.cc
的上层封装开始,层层深入,直至 memtable/inlineskiplist.h
中跳表的实现。
第一部分:SkipListRep
- MemTable
的接口与工厂
memtable/skiplistrep.cc
定义了 MemTable
的默认实现。MemTable
是 RocksDB 用于在内存中缓存写入的有序数据结构。SkipListRep
便是基于跳表(Skip List)的 MemTable
表示(Representation)。
1.1 SkipListRep
类的结构
SkipListRep
继承自 MemTableRep
,它是一个接口类,定义了 MemTable
必须具备的行为。
// in memtable/skiplistrep.cc
class SkipListRep : public MemTableRep {
InlineSkipList<const MemTableRep::KeyComparator&> skip_list_;
const MemTableRep::KeyComparator& cmp_;
const SliceTransform* transform_;
const size_t lookahead_;
friend class LookaheadIterator;
public:
explicit SkipListRep(const MemTableRep::KeyComparator& compare,
Allocator* allocator, const SliceTransform* transform,
const size_t lookahead)
: MemTableRep(allocator),
skip_list_(compare, allocator),
cmp_(compare),
transform_(transform),
lookahead_(lookahead) {}
// ... Methods like Insert, Get, GetIterator ...
};
skip_list_
: 核心数据成员,一个InlineSkipList
的实例,所有数据和主要操作都在这里。我们将在第二部分深入它。cmp_
: 键比较器,决定了数据在跳表中的排序方式。lookahead_
: 用于LookaheadIterator
的优化参数,我们将在迭代器部分详述。
SkipListRep
的方法大多是简单的“转发器”,将调用直接委托给 skip_list_
,例如:
// in memtable/skiplistrep.cc
void SkipListRep::Insert(KeyHandle handle) {
skip_list_.Insert(static_cast<char*>(handle));
}
bool SkipListRep::Contains(const char* key) const {
return skip_list_.Contains(key);
}
1.2 SkipListFactory
:可插拔的工厂模式
RocksDB 使用工厂模式来创建 MemTable
。
// in memtable/skiplistrep.cc
class SkipListFactory : public MemTableRepFactory {
public:
explicit SkipListFactory(size_t lookahead = 0) : lookahead_(lookahead) {}
MemTableRep* CreateMemTableRep(
const MemTableRep::KeyComparator& compare, Allocator* allocator,
const SliceTransform* transform, Logger* /*logger*/) override {
return new SkipListRep(compare, allocator, transform, lookahead_);
}
// ...
private:
const size_t lookahead_;
};
CreateMemTableRep
方法清晰地展示了如何实例化一个 SkipListRep
,并将配置参数 lookahead_
传递进去。
第二部分:InlineSkipList
- 高性能数据结构核心
InlineSkipList
的实现在 memtable/inlineskiplist.h
。
2.1 核心优化:内联存储、Arena 与无锁并发
InlineSkipList
的性能源于三大优化设计的协同工作:
1. 内联数据存储 (Inline Data Storage): 这是为了最大化 CPU 缓存命中率的优化。它通过 C++ 的柔性数组成员(Flexible Array Member)技巧实现,将节点元数据和 Key-Value 数据放在一块连续的内存中。
// in memtable/inlineskiplist.h
struct Node {
// Array of next pointers. next_[0] is lowest level link.
std::atomic<Node*> next_[1];
// Key contents are stored after the Node structure in the memory.
char key[0];
};
next_[1]
是一个占位符,实际分配内存时会根据节点高度动态扩展此数组的大小。key[0]
标记了 Key 数据的起始地址,它紧跟在next_
数组之后。- 这种布局意味着当 CPU 读取
Node
的指针时,Key 的数据也被一同加载到缓存,极大地加速了后续的 Key 比较操作。 - 内存布局示意:
[ Node 结构(next 指针等) | Key 的数据 | Value 的数据 ]
2. Arena 内存分配: 所有 Node
都通过 Arena
(内存池)进行分配,避免了 malloc
的系统调用开销,减少了内存碎片,并因物理内存的连续性进一步提升了缓存友好性。
3. 无锁并发: next_
指针是 std::atomic<Node*>
类型,所有修改都通过原子的 CAS(Compare-And-Swap) 操作完成,无需使用重量级的互斥锁。这使得多线程可以高效地并发插入,是 RocksDB 高写入吞- 吞吐量的关键。
第三部分:迭代器 - 遍历与 Seek
3.1 InlineSkipList::Iterator
的实现
迭代器封装了遍历逻辑,其实现非常轻量。
// in memtable/inlineskiplist.h
class Iterator {
public:
explicit Iterator(const InlineSkipList* list);
bool Valid() const;
const char* key() const;
void Next();
void Prev();
void Seek(const char* target);
void SeekToFirst();
void SeekToLast();
private:
const InlineSkipList* list_;
Node* node_;
};
- 状态: 仅包含
list_
和node_
两个指针,node_
指向当前位置。 Next()
: O(1) 操作,原子地读取node_->Next(0)
。Prev()
: O(log N) 操作,因跳表是单向的,需调用list_->FindLessThan(key())
从头查找前驱。Seek(target)
: O(log N) 操作,直接调用list_->FindGreaterOrEqual(target)
来定位。
3.2 LookaheadIterator
的优化
SkipListRep
在 GetIterator
时,如果 lookahead_ > 0
,会创建一个 LookaheadIterator
。这是一个优化版本,它的 Seek
方法很智能:
// in memtable/skiplistrep.cc
void Seek(const Slice& internal_key, const char* memtable_key) {
// ...
if (prev_.Valid() && rep_.cmp_(encoded_key, prev_.key()) >= 0) {
// 如果目标 key 在上一个位置之后,尝试进行短距离线性扫描
iter_ = prev_;
size_t cur = 0;
while (cur++ <= rep_.lookahead_ && iter_.Valid()) {
if (rep_.cmp_(encoded_key, iter_.key()) <= 0) {
return; // 找到了!避免了一次完整的 Seek
}
Next();
}
}
// 如果线性扫描失败,再执行完整的 Seek
iter_.Seek(encoded_key);
prev_ = iter_;
}
这种设计利用了访问的局部性原理,用很小的代价优化了常见的连续 Seek
场景。
3.2 核心查找算法: FindGreaterOrEqual
源码剖析
Iterator->Seek()
就是调用的是InlineSkipList
的FindGreaterOrEqual
函数。
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {
node_ = list_->FindGreaterOrEqual(target);
}
此函数是跳表查找的核心,完美展现了其 O(log N) 的查找效率。
// in memtable/inlineskiplist.h
template <class Comparator>
inline typename InlineSkipList<Comparator>::Node*
InlineSkipList<Comparator>::FindGreaterOrEqual(const char* key, Node** prev) const {
// 1. 初始化:从虚拟头节点的最高层开始
Node* x = head_;
int level = GetMaxHeight() - 1;
// 2. 主循环:从高层向低层逐级逼近
while (level >= 0) {
// 3. 内部循环:在当前层向右前进,直到下一个节点不小于 key
Node* next = x->Next(level);
while (next != nullptr && KeyIsAfterNode(key, next)) {
x = next;
next = x->Next(level);
}
// 4. 记录前驱节点(为插入做准备),并下降一层
if (prev != nullptr) {
prev[level] = x;
}
level--;
}
// 5. 返回结果:在最底层找到的前驱节点的下一个节点
return x->Next(0);
}
这个阶梯式的查找路径(高层大步前进,低层精细查找)是跳表高效的根源。
3.3 Seek
为什么是“大于或等于”?
Seek
找到“第一个大于或等于 target 的节点”是完全正确的,这与 RocksDB 的内部键(Internal Key)设计密切相关。
- 内部键结构:
[ User Key | Sequence Number | Operation Type ]
- 排序规则: 1. 按
User Key
升序; 2. 若User Key
相同,则按Sequence Number
降序。
最新的操作(Sequence Number
最大)反而排在最前面。当 Seek("my_key")
时,RocksDB 查找的是 ("my_key" | MAX_SEQ_NUM)
,FindGreaterOrEqual
会根据排序规则精确地找到 "my_key"
的最新版本,一步到位。
第四部分:工程实践 - UniqueRandomSample
的自适应算法
SkipListRep::UniqueRandomSample
用于数据采样,这个功能对于数据库收集统计信息、进行查询优化等非常重要。
// in memtable/skiplistrep.cc
void SkipListRep::UniqueRandomSample(...) {
// ...
if (target_sample_size > static_cast<uint64_t>(std::sqrt(1.0 * num_entries))) {
// 采样数较大时,遍历一遍,按概率采样
// ...
} else {
// 采样数较小时,执行多次随机跳转来采样
// ...
}
}
它内部根据输入参数,动态地选择了两种不同的采样算法,以追求最佳性能。
当采样数
m
比较大时 (m > sqrt(N)
):- 它会从头到尾完整地遍历一遍跳表。
- 对于遍历到的第 i 个元素,它以
(m' / (N-i))
的概率决定是否将其放入样本集(其中m'
是还需要采样的数量)。这是一种经典的“水塘采样”变体,保证了遍历一遍后能得到一个近似大小的无偏样本集。
- 当采样数
m
比较小时 (m < sqrt(N)
):- 完整遍历一遍的成本太高了。
- 此时,它会执行
m
次RandomSeek()
(随机跳转),直接随机跳到 m 个节点来作为样本。 - 它使用一个
unordered_set
来自动处理随机跳转时可能产生的重复。
它根据采样数量和数据总量的关系,动态选择成本更低的算法,体现了在真实场景中对算法性能的深刻理解和权衡。