LevelDB的SkipList实现

318 阅读8分钟

leveldb 中的 SkipList 具体实现在db/skiplist.h中。

私有域:

  1. struct Node 用来存储数据,和上面 Java 实现的不同,Node 中 key 的值既包含了需要查询的 key,也包含具体的 Value,这个和数据存储格式相关系
  2. enum { kMaxHeight = 12 }; 允许最大的层数是 12 层
  3. compare_ 用来做 key 值比较的比较器
  4. arena_ 为每个 Node 分配内存
  5. head_ 头结点指针
  6. std::atomic maxheight; 原子递增的最大层数
  7. 一个上文提到的随机数生成的对象

explicit 关键字一般用来修饰类的构造函数,作用是告诉编译器按照实际的类型来构造函数,不允许做隐式转换。

公有域:

  1. 一个不允许隐式转换的构造器

  2. void Insert(const Key& key); // 插入方法

  3. bool Contains(const Key& key) const; 判断是否存在

  4. Iterator 一个迭代器实现,迭代器中包含了多种查询方式,包括查找当前的 key 等方法(后文详细介绍)。其中也包含了私有域,指向当前的 SkipList 和一个用来遍历的 Node。

Node 结构体包含了一个 Key,这个就是前面提到的template <typename Key, class Comparator>中的这个 Key,还包含了一个数组

std::atomic<Node*> next_[1];

这个是一种柔性的数组。也就是大小是可变的,next 数组是一个指针数组,也就是上文中 java 代码中的 IndexNode,本身不存储 key Value,而是作为索引存储。这个 next 表示当前层数中指向的下一个节点。

一个 SkipList 初始化过程中需要传入的参数就是比较器 comparator* 和内存分配器 arena*的指针。

查询

和上面的实现不同的是,leveldb 的查询是放在 Iterator 的 seek 方法中的

template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {
  node_ = list_->FindGreaterOrEqual(target, nullptr);
}

最后调用的是 SkipList 本身的 FindGreaterOrEqual 方法,也就是要么找到当前值,要么就找到比当前值大的那个值。该方法包含了两个参数,第一个是需要查询的值,第二个是用来存储当前查询 level 的前序节点,也就是 update 数组。

template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::KeyIsAfterNode(const Key& key, Node* n) const {
  // null n is considered infinite
  return (n != nullptr) && (compare_(n->key, key) < 0);
}

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
                                              Node** prev) const {
  Node* x = head_; // x为后续查询使用的临时数据,此时x在最上层的头结点
  int level = GetMaxHeight() - 1; //获取当前最大的层数,从0开始,所以减去1
  while (true) {
    Node* next = x->Next(level); // next数组就是每一层链表的数据,从head开始
    if (KeyIsAfterNode(key, next)) { //找到当前的查询的值是否在x后面,如果是,则说明当前的值的后一个值小于查询的值
      // Keep searching in this list
      x = next; //往右查询。
    } else { // 如果x的后面的值大于查询的key,则判断是是否需要记录层数,get中prev是nullptr,所以不需要存储,在insert的时候就需要记录下来
      if (prev != nullptr) prev[level] = x;
      if (level == 0) { // 如果当前已经查询到最后一层,则返回这个next,此时next因为前面的KeyIsAfterNode 返回的是false,所以要么当前的值的右边为nullptr要么这个值就比key大,而且是紧挨的着的那个大于这个key的值。仅仅在第0层是这个结果
        return next;
      } else {
        // Switch to next list
        level--;
      }
    }
  }
}

本身实现上和上文的没有区别,首先从最高层往下便利,其中 Next 方法就是获取到下一层的节点。这个方法还有一个实现就是NoBarrier_Next,两者的区别就是是否允许重排,在 java 中,指令重排是使用 valite 关键字关闭,c++中用下面的方式:(直接使用 leveldb 的代码)

  Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].store(x, std::memory_order_release);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_relaxed);
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_relaxed);
  }

其中的 memory_order 就是决定当前的指令是否重排:

  1. std::memory_order_acquire:这是一种内存顺序语义,用于 load 操作。它确保在加载 next_[n] 指针之前,所有之前的内存操作(写入和读取)都不会被重排序到加载之后。在多线程环境中,这可以确保读取到 next_[n] 的值是最新的。
  2. std::memory_order_release:这是一种内存顺序语义,用于 store 操作。它确保在存储 xnext_[n] 指针之后,所有之后的内存操作(写入和读取)都不会被重排序到存储之前。在多线程环境中,这可以确保 x 被完全初始化之后再被其他线程访问。
  3. std::memory_order_relaxed:这是一种较弱的内存顺序语义,通常用于 loadstore 操作。它允许更大的重排序自由度,不保证像 acquirerelease 那样的强制顺序。通常在不需要严格的顺序控制时使用,以提高性能。

所以 setNext 值得是当前的获取的 n 是否被后续访问的线程可见,避免了出现多次创建甚至覆盖的问题,但是后面的两个则没有这个限制。

来看个例子:

当前如果有:

head2--->3->

head1--->3->   5->   9->

head0--->3->4->5->7->9->

如果有上文中的 3 层,现在需要查询 8

  1. 首先 x 被复制为 head2 进入循环,然后 x 获取 head 的链表也就是 next 中的数据,此时为 3
  2. 当前查询的 8 大于 3 ,所以将 3 位置 Node 赋值给 x,进入到下次循环
  3. 继续获取 3 后面的值,但是此时 3 后面的值为空,所以 KeyIsAfterNode 返回为 false,进入到 else,判断 prev 是否存储,判断是否已经到了第 0 层,此时否为 false,所以将 level--,找下一层
  4. x 此时获取的是 next 中的第 2 层,也就是 head1 后面的 3,因为 3 小于 8,所以找到了 5,所以将 5 赋值给 x,继续循环
  5. 此时 x 为 5,里面的 next 指向的是 9,也就是 next 此时为 9,9 大于 8。所以不在赋值给 x。而是进入下一层
  6. 进入到 head0,此时 x 为 5,next 为 7,然后 7 小于 8,进入下次循环
  7. 此时 x 为 7,但是 next 为 9,因为是 0 层了,所以直接返回 next

也就是说这个方法返回的值和他的名字一样,和当前值相等或者比当前值大的第一个数。在 Iterator 中 seek 的值会存储在本身的 node_ 属性中,需要进一步进行判断。如果这个值为 nullptr,则 Iterator 中的 Valid 为 false。

插入

查询也使用到了上面的 FindGreaterOrEqual

template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  Node* x = FindGreaterOrEqual(key, prev);

  // Our data structure does not allow duplicate insertion
  assert(x == nullptr || !Equal(key, x->key));

  int height = RandomHeight();
  if (height > GetMaxHeight()) {
    for (int i = GetMaxHeight(); i < height; i++) {
      prev[i] = head_;
    }
    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (nullptr), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since nullptr sorts after all
    // keys.  In the latter case the reader will use the new node.
    max_height_.store(height, std::memory_order_relaxed);
  }

  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
    const Key& key, int height) {
  char* const node_memory = arena_->AllocateAligned(
      sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
  return new (node_memory) Node(key);
}

插入过程中,首先是找到已经存在的 key,或者这个 key 的后缀节点,需要注意的是这个 key 再 leveldb 中时不允许在一个一模一样的 key 已经存在然后继续插入的。leveldb 中的每次插入都包含了一个序列,所以这个 key 一般不会一样,我能想到的完全一样的情况感觉就是 sequence 的值获取是出现问题,导致一个 key 拥有了相同的 sequence,这个后面再讨论。

然后就是获取到一个随机的高度,如果当前的高度超过了现在最大的高度(random 不会返回超过 12 的高度),就需要将在获取 key 的时候存储的 prev 节点添加上新的 head 节点。然后更新高度。将当前的 key 和 height 封装为一个新的 Node,主要是开辟内存空间。然后就是填充各个层的数据

删除

leveldb 本身的删除就是一个添加墓碑标记的删除,后续文件合并才会真正的删除,所以 Skiplist 中也没有删除。

迭代方式

SkipList 的迭代器中的方法有:

    bool Valid() const; // 查询的结果是否是nullptr
    // Advances to the next position.
    // REQUIRES: Valid()
    void Next(); // 找到查询值的后续节点

    // Advances to the previous position.
    // REQUIRES: Valid()
    void Prev();// 查询值的前序节点

    // Advance to the first entry with a key >= target
    void Seek(const Key& target); // 获取到key>=target的值

    // Position at the first entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    void SeekToFirst(); // 最小值

    // Position at the last entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    void SeekToLast(); // 最大值

其中 Valid 、 Next 和 Seek 已经大概说过了。主要就是一个 Prev,SeekToFirst 和 SeekToLast。

SeekToLast 的方法比较简单,就是从 level 为 0 一直遍历到最最后面。SeekToFirst 实现就是找到第一个,也就是获取 level 0 的第一个值。所以这个两个值可以算得上是获取最大最小值。

Prev 则是找到当前查询值小的最后的一个值:

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindLessThan(const Key& key) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    assert(x == head_ || compare_(x->key, key) < 0);
    Node* next = x->Next(level);
    if (next == nullptr || compare_(next->key, key) >= 0) {
      if (level == 0) {
        return x;
      } else {
        // Switch to next list
        level--;
      }
    } else {
      x = next;
    }
  }
}

核心实现就是上面的 FindLessThan,可以看到和前面的 FindGreaterOrEqual 类似,FindGreaterOrEqual 已经很详细的说过,这里就不赘述了。