前言
最近在研究学习leveldb的源码,并且尝试用Rust进行重写leveldb-rs,leveldb中memdb模块是使用skiplist作为一个kv的内存存储,相关代码实现非常漂亮,所以有了这篇文章。 leveldb通过使用Arena模式来实现skiplist。简单来说,就是利用线性数组来模拟节点之间的关系,可以有效避免循环引用。
- c++版本的leveldb虽然也是使用的arena模式,但是节点数据内存的申请和访问进行了封装,skiplist的结构定义和实现跟传统意义上的skiplist的代码实现非常相似,如果如果大家之前了解过skiplist的话,c++版本的代码是非常容易看懂的。
- golang版本leveldb 缺乏arena的封装,直接操作slice,如果对arena模式不熟悉的话,理解起来就比较麻烦。从软件工程角度上开,golang版本的memdb的代码写的不太好,可以进一步优化的和重构arena的操作。
在本文中将会讲解下面内容:
- 对比c++和golang版本中查询、插入、删除的实现
- 分析golang版本中可以优化的地方
然后在下一篇文章中将会介绍
- 基于golang版本使用rust重写memdb(arena版本)
- 使用rust重写一个非arena版本的memdb,也就是经典的链表结构实现方式
类型声明
首先我们来对比C++和Golang的代码中的skiplist定义:
C++
https://github.com/google/leveldb/blob/master/db/skiplist.h#L41
这里主要列出关键的成员变量,详细的可以去看源码:
template <typename Key, class Comparator>
class SkipList {
...
// Immutable after construction
Comparator const compare_;
Arena* const arena_; // Arena used for allocations of nodes
Node* const head_;
// Modified only by Insert(). Read racily by readers, but stale
// values are ok.
std::atomic<int> max_height_; // Height of the entire list
// Read/written only by Insert().
Random rnd_;
};
- Comparator const compare_; 用来在遍历skiplist进行节点key的比较
- Arena* const arena_; 使用Arena模式的内存管理
- Node* const head_; 首节点
- std::atomic max_height_; skiplist的层高,在插入的时候可能会变化
- Random rnd_; 随机数生成器,用于在每次插入的时候生成新节点的层高
Golang
https://github.com/syndtr/goleveldb/blob/master/leveldb/memdb/memdb.go#L183
type DB struct {
cmp comparer.BasicComparer
rnd *rand.Rand
mu sync.RWMutex
kvData []byte
nodeData []int
prevNode [tMaxHeight]int
maxHeight int
n int
kvSize int
}
- cmp comparer.BasicComparer :用来在遍历skiplist进行节点key的比较
- rnd *rand.Rand: 随机数生成器,用于在每次插入的时候生成新节点的层高
- kvData []byte: key和value实际数据存放的地方
- nodeData[]int: 存储各个节点的信息
- prevNode [tMaxHeight]int: 用于在遍历skiplist的时候,保存每一层的前一个节点
- maxHeight int: skiplist的层高,在插入的时候可能会变化
- n int: 节点的总个数
- kvsize: skiplist中存储key和value的总字节数
golang版本里面最难理解的就是nodeData, 只有理解了nodeData的数据布局,后面代码就容易理解了。
- kvData中存储的是key,value的真实的字节数据
- kvNode中存储的是skiplist中的全部节点,但是节点不存储key和value的实际数据而是在Kvdata中的偏移以及key的长度,value的长度,在比较的时候再根据偏移和长度到KvData中读取。另外KvNode中还存储了当前节点的层高,以及每一层的下一个节点在KvNode中的偏移量,在查询的时候,就可以根据偏移量跳到KvNode中下一个节点的位置,在从里面读取信息
查询大于等于特定Key
首先看skiplist中的查询,leveldb中查询的实现是最关键的,插入和删除也都是基于查询实现,我们先来简单回顾下查询的过程:
- 首先根据跳表的高度选取最高层的头节点;
- 若跳表中的节点内容小于查找节点的内容,则取该层的下一个节点继续比较;
- 若跳表中的节点内容等于查找节点的内容,则直接返回;
- 若跳表中的节点内容大于查找节点的内容,且层高不为0,则降低层高,且从前一个节点开始,重新查找低一层中的节点信息;若层高为0,则返回当前节点,该节点的key大于所查找节点的key
我们举例来说,如果要在下面的skiplist中查询key为17节点
- 从最左边的head节点开始,当前层高是4;
- head节点在第4层的next节点的key是6,由于 17 大于6,所以在当前节点的右边,就沿着当前层的链表走到下一节点,也就是key是6节点。
- 6节点 在第4层的next节点是NIL,也就是后面没有节点了,那么就需要在当前节点往下层走,走到第3层。
- 6节点 在第3层的next节点的key是25,由于 17 小于25,那么就需要在当前节点往下层走,走到第2层。
- 6节点 在第2层的next节点的key是9,由于 17 大于9,那么就沿着当前层的链表走到下一节点,也就是key是9的节点。
- 9节点 在第2层的nex节点的key是25,由于 17 小于25,那么就需要在当前节点往下层走,走到第1层。
- 8节点 在第1层的next节点的key是12,由于 17 大于12,那么就沿着当前层的链表走到下一节点,也就是key是12的节点。
- 12节点 在第1层的next节点的key是19,由于 17 小于19,本来应该要继续走到下一层,但是由于当前已经是最后一层了,所以直接返回12的next节点,也就是19节点
C++
https://github.com/google/leveldb/blob/master/db/skiplist.h#L260
在skiplist中查询大于等于key的最小节点的方法如下
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
Node** prev) const {
Node* x = head_; // head节点
int level = GetMaxHeight() - 1;// 当前层高
while (true) {
Node* next = x->Next(level);
if (KeyIsAfterNode(key, next)) {
// 如果当前层中x的下一个节点的key小于key
x = next; // 继续在当前层的list往后搜索
} else {
if (prev != nullptr) prev[level] = x; // 如果要记录遍历过程中的pre节点,就记录
if (level == 0) {
// 搜索到底了就返回
return next;
} else {
// 如果当前层中x下一个节点的key大于key,往下一层进行搜索
level--;
}
}
}
}
Go
https://github.com/syndtr/goleveldb/blob/master/leveldb/memdb/memdb.go#L211
在skiplist中查询大于等于key的最小节点的方法如下
// Must hold RW-lock if prev == true, as it use shared prevNode slice.
func (p *DB) findGE(key []byte, prev bool) (int, bool) {
node := 0 // head 节点
h := p.maxHeight - 1 // 当前层高
for {
next := p.nodeData[node+nNext+h]
cmp := 1
if next != 0 {
o := p.nodeData[next]
cmp = p.cmp.Compare(p.kvData[</