leveldb memdb源码分析(上)

前言

最近在研究学习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中下一个节点的位置,在从里面读取信息

leveldb memdb源码分析(上)_第1张图片
查询大于等于特定Key

首先看skiplist中的查询,leveldb中查询的实现是最关键的,插入和删除也都是基于查询实现,我们先来简单回顾下查询的过程:

  • 首先根据跳表的高度选取最高层的头节点;
  • 若跳表中的节点内容小于查找节点的内容,则取该层的下一个节点继续比较;
  • 若跳表中的节点内容等于查找节点的内容,则直接返回;
  • 若跳表中的节点内容大于查找节点的内容,且层高不为0,则降低层高,且从前一个节点开始,重新查找低一层中的节点信息;若层高为0,则返回当前节点,该节点的key大于所查找节点的key

我们举例来说,如果要在下面的skiplist中查询key为17节点

leveldb memdb源码分析(上)_第2张图片

  • 从最左边的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

你可能感兴趣的:(DEEPNOVA开发者社区,数据库,rust,golang)