LevelDB的SkipList实现

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 max_height_; 原子递增的最大层数
  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,而是作为索引存储。他存储的数据是一个包含了Key值的Node的指针,当前Node有多少层,他的数组就是多大。也就是记录了每一层的数据。

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

查询

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

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

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

arduino 复制代码
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的代码)

scss 复制代码
  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是否被后续访问的线程可见,避免了出现多次创建甚至覆盖的问题,但是后面的两个则没有这个限制。

来看个例子:

当前如果有:

rust 复制代码
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

scss 复制代码
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的迭代器中的方法有:

csharp 复制代码
    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则是找到当前查询值小的最后的一个值:

arduino 复制代码
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_(xeveldb中的SkipList具体实现在`db/skiplist.h`中。

私有域:

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

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

公有域:

1.  一个不允许隐式转换的构造器
1.  void Insert(const Key& key); // 插入方法
1.  bool Contains(const Key& key) const; 判断是否存在
1.  Iterator 一个迭代器实现,迭代器中包含了多种查询方式,包括查找当前的key等方法(后文详细介绍)。其中也包含了私有域,指向当前的SkipList和一个用来遍历的Node。

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

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

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

一个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); }

sql 复制代码
最后调用的是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--; } } } }

r 复制代码
本身实现上和上文的没有区别,首先从最高层往下便利,其中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); } ​

go 复制代码
其中的memory_order就是决定当前的指令是否重排:

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

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

来看个例子:

当前如果有:

head2--->3-> ​ head1--->3-> 5-> 9-> ​ head0--->3->4->5->7->9->

vbnet 复制代码
如果有上文中的3层,现在需要查询8

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

vbnet 复制代码
插入过程中,首先是找到已经存在的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(); // 最大值

sql 复制代码
其中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; } } }

vbnet 复制代码
核心实现就是上面的FindLessThan,可以看到和前面的FindGreaterOrEqual类似,FindGreaterOrEqual已经很详细的说过,这里就不赘述了。->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已经很详细的说过,这里就不赘述了。

相关推荐
雷渊19 分钟前
深入分析Spring的事务隔离级别及实现原理
java·后端·面试
Smilejudy29 分钟前
不可或缺的相邻引用
后端
惜鸟29 分钟前
Elasticsearch 的字段类型总结
后端
rebel31 分钟前
Java获取excel附件并解析解决方案
java·后端
微客鸟窝33 分钟前
Redis常用数据类型和命令
后端
熊猫片沃子34 分钟前
centos挂载数据盘
后端·centos
微客鸟窝35 分钟前
Redis配置文件解读
后端
不靠谱程序员37 分钟前
"白描APP" OCR 软件 API 逆向抓取
后端·爬虫
小华同学ai39 分钟前
6.4K star!企业级流程引擎黑马,低代码开发竟能如此高效!
后端·github
Paladin_z42 分钟前
【导入导出】功能设计方案(Java版)
后端