LevelDB 浅谈1--SkipList(跳跃表)

455 阅读2分钟

跳跃表的具体原理,请参考: www.huliujia.com/blog/071730…

SkipList

SkipList是LevelDB的MemTable的底层存储结构。MemTable不会进行删除元素,所以在SkipList中没有删除元素的接口。首先看SkipList对外开放接口:

template <typename Key, class Comparator>
class SkipList {
 private:
  struct Node;

 public:
  explicit SkipList(Comparator cmp, Arena* arena);
  SkipList(const SkipList&) = delete;
  SkipList& operator=(const SkipList&) = delete;
  void Insert(const Key& key);
  bool Contains(const Key& key) const;
private:
  // 比较器
  Comparator const compare_;
  Arena* const arena_;  // Arena used for allocations of nodes
  // skiplist 的前置哨兵节点
  Node* const head_;
};

首先看它的模板信息,其中 Key 是存储的元素类型,Comparator是元素比较器。

SkipList对外有两个接口:

  // 插入操作
  void Insert(const Key& key);
  // 查看key是否存在
  bool Contains(const Key& key) const;

本篇详细讲解一下插入接口。在讲解 Insert 接口前,我们需要了解,SkipList插入的数据是Key类型,那么在SkipList中是如何存储的呢?

跳跃表的每个元素存储在 Node 结构中,Node之间相互链接就形成了跳跃表。在 SkipList 类中维护跳跃表的头节点 head_。我们可以通过 head_找到跳跃表中任何一个元素。

image.png

图1

Node 类实现如下:

template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}
  // 存储插入到跳跃表的元素
  Key const key;
  // 获取该节点,后面的第n层节点
  Node* Next(int n);
  // 设置 该节点在第n层的后置节点为 x
  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);
  }
  // 获取该节点在第n层的后置节点
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_relaxed);
  }
  //// 设置 该节点在第n层的后置节点为 x
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_relaxed);
  }

 private:
  // // 指针数组的长度即为该节点的 level,next_[0] 是最低层指针.
  // Array of length equal to the node height.  next_[0] is lowest level link.
  std::atomic<Node*> next_[1];   // 指向指针数组的指针
};

每个Node节点中维护一个 next_[kMaxHeight],每个数组元素为 Node*,指向该层下一个节点。

我认为levelDB中SkipList采用如此结构,可以节省内存空间,所有的元素只需要存储一次,利用next_数组维护层结构。

注意:在初始化一个新的Node节点时,levelDB采用了“定位构造的方法”,在Node类声明中先将 next_数组大小设置为1,其实这个1,只是起了一个占位的作用,实际大小是在构造函数中确定的。

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
    const Key& key, int height) {
  // 对 柔性数组(std::atomic<Node*> next_[1];) 进行申请内存
  char* const node_memory = arena_->AllocateAligned(
      sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
  // 定位构造
  // placement new就是在用户指定的内存位置上构建新的对象,
  // 这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可
  return new (node_memory) Node(key);
}