首页 > 编程语言 >LevelDB源码剖析(3) Skiplist跳表

LevelDB源码剖析(3) Skiplist跳表

时间:2022-11-29 01:11:08浏览次数:53  
标签:Node LevelDB const next 跳表 key 节点 源码

1. 背景

什么是跳表?跳表是SortedMap的一种具体实现,是一种概率性的数据结构。跳表拥有SortedMap的所有功能,定位和红黑树类似,其和红黑树的区别在于
优点:

  • 跳表的实现更加简单
  • 跳表在范围查询的时候效率是要高于红黑树
  • 红黑树的插入和删除可能引发子树的调整,逻辑比较复杂,跳表只要维护相邻节点。

缺点:

  • 跳表需要维护额外的多层链表,是一种空间换时间的做法,相比之下红黑树不需要占用多余的空间

2. 原理

跳表的每个节点有一个随机产生的层数,从1层到n层,每一层的节点单独组成一个链表。每层链表之间的节点都是按照顺序排列的,并且自底向上节点越来越稀疏,第一层包含了所有的数据,查找的时候层数自顶向下逐渐下降,直到找到对应的节点。
image.png
以上图为例,假设我们需要查找key为35的数据流程就是

  • 以HEAD开始,先从Level 3层查找,发现35 < 37, 则查找层数降为Level 2
  • 在Level 2层发现35 > 13,则跳转到13
  • 在13节点继续从Level 2层查找,发现35 < 37, 将层数降为Level 1
  • 以此类推,直至在Level 0层查找到35

3. 源码解析

跳表的原理并不复杂,所以侧重点主要放在实现上。

3.1 SkipList.h

先看跳表对外的接口,跳表提供的主要功能是对元素的插入和查询并提供了一个迭代器以便进行范围查找

template <typename Key, class Comparator>
class SkipList {
 private:
  struct Node;
 public:
  //创建一个跳表,用cmp进行比较,arena进行内存分配
  explicit SkipList(Comparator cmp, Arena* arena); //explicit的目的在于与防止类构造函数的隐式自动转换
  SkipList(const SkipList&) = delete; //跳表类禁止拷贝
  SkipList& operator=(const SkipList&) = delete;
  void Insert(const Key& key); //向跳表中插入一个key,需要保证表中没有与插入数据相同的key
  bool Contains(const Key& key) const; //判断key是否存在跳表中
  //跳表的迭代器
  class Iterator {
   public:
    explicit Iterator(const SkipList* list); //用跳表初始化迭代器
    bool Valid() const; //节点是否可用
    const Key& key() const; //返回key
    void Next(); //进入下一个节点
    void Prev(); //进入上一个节点
    void Seek(const Key& target); //进入第一个key >= target的节点
    void SeekToFirst(); //进入第一个节点
    void SeekToLast(); //进入最后一个节点
   private:
    const SkipList* list_;
    Node* node_;
  };
}

3.2 FindGreaterOrEqual

跳表内实现了多个查询函数,思路基本相同,这边选取一个比较有代表性的查询函数,即查找第一个大于等于key的节点的指针。

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
                                              Node** prev) const {
  Node* x = head_; //使用头节点作为起点
  int level = GetMaxHeight() - 1;
  while (true) { 
    Node* next = x->Next(level);
    if (KeyIsAfterNode(key, next)) { //判断key是否大于下一个节点
      // Keep searching in this list
      x = next; //如果大于则继续前进
    } else {
      if (prev != nullptr) prev[level] = x;
      if (level == 0) { //如果到第0层说明到底了
        return next;
      } else {
        // Switch to next list
        level--;      //否则进行层数下降继续查找
      }
    }
  }
}

3.3 Insert

插入的基本逻辑是找到key大于等于的那个key的节点,并将其插入到结点之后。

template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  Node* prev[kMaxHeight]; //前置node列表
  Node* x = FindGreaterOrEqual(key, prev);
  assert(x == nullptr || !Equal(key, x->key));

  int height = RandomHeight(); //随机节点的高度
  if (height > GetMaxHeight()) {
    for (int i = GetMaxHeight(); i < height; i++) {
      prev[i] = head_;
    }
    max_height_.store(height, std::memory_order_relaxed);
  }

  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}

值得一提的是其中实现了NoBarrier_SetNext和SetNext两个功能相同但实现有区别的函数,其中NoBarrier_SetNext是没有内存屏障的操作,通过使用宽松顺序对next_n进行读写来提高性能,但代价是这是性能不安全的,那为什么仍然使用这种方式呢?
从代码中可以看出,节点的插入主要分为两步,即

  1. x->next = pre->next
  2. pre->next = x

对于第一步操作而言,该操作无需对所有内存可见,因为此时x还没有完全插入链表中,其他内存依然可以通过pre正常访问到pre->next, 其他线程依然可以正常读取。无需关注x。
而到了第二步,由于这步会对pre->next进行修改,再修改过程中如果其他内存访问pre->next, 可能会得到错误的结果,因此这步使用了线程安全的SetNext。

3.4 Node

Node是很多数据结构都会有的节点类,这这边主要关注两点

  1. 使用宽松内存顺序NoBarrier_SetNext的实现
  2. Node对象的内存分配,因为节点的高度不确定,所以next的长度也无法确定,这里采用的方式是在NewNode中连续申请一块堆内存,包含了对象以及其成员所需的内存,然后next_就可以使用动态的长度
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}
  Key const key;
  Node* Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_release);
  }
  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);
  }


 private:
  std::atomic<Node*> next_[1]; //由于Node是在New Node函数中使用堆内存分配,所以next_实际可以访问数组定义之外的内存,这里用长度为[1]的数组只是为了方便理解
};


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)); //申请Node本身以及next_的内存
  return new (node_memory) Node(key);
}

3.5 RandomHeight

在原理中有提到,跳表的高度是越往上越稀疏的,实操的时候是怎么控制稀疏程度的呢?通过代码可以看到,实际使用的时候,通过控制每个节点Height的分布,使得RandomHeight的返回满足“高度越高,数量越少”的原则,从期望上就可以达到越往上越稀疏的结构。实际开发代码的过程中,是以每次有四分之一的概率网上累加这一原则,对Height进行随机的,就可以保证每层的节点都是上一层的三分之一左右。

template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
  // Increase height with probability 1 in kBranching
  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxHeight && rnd_.OneIn(kBranching)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxHeight);
  return height;
}

标签:Node,LevelDB,const,next,跳表,key,节点,源码
From: https://www.cnblogs.com/Hugh-Locke/p/16934281.html

相关文章

  • 深入理解Kubernetes 4A - Audit源码解析
    Overview本文是关于Kubernetes4A解析的第四章深入理解Kubernetes4A-Authentication源码解析深入理解Kubernetes4A-Authorization源码解析深入理解Kubernetes......
  • cocos2d-x 是男人就下100层 附源码
    1.效果图:  玩法:一个不断下降的小人,点击屏幕的left或者right控制小人的移动方向,尽可能生存久些. 为什么要搞这个游戏呢?因为在2012年的8月份,我完成它的android版本,......
  • C++学习------cmath头文件的源码学习06
    函数族定义---双曲函数cosh---计算双曲余弦函数sinh---计算双曲正弦函数tanh---计算双曲正切函数acosh---计算双曲余弦面积asinh---计算双曲正弦面积atanh---计算双曲正切面......
  • go源码学习(一):数据结构-数组
    数组是相同类型元素的集合,在内存中对应一块连续的内存空间。数组类型是通过存储的元素类型以及能够存储的大小两个维度来决定的,一旦声明之后大小就不可更改。初始化go语......
  • SpringBoot 自动装配源码解析
    SpringBoot自动装配源码解析step1:SpringApplication.run(ZylSpringBootApplication.class,args);step2:this.refreshContext(context);-->org.springframework.bo......
  • jsp源码实例2(获取表单参数)
    jsp源码实例2(获取表单参数)packagecoreservlets;importjava.io.*;importjavax.servlet.*;importjavax.servlet.http.*;importjava.util.*;/**Showsallthep......
  • pinia源码解读二(定义模块)
    定义模块store.ts文件的defineStore方法判断是option写法还是setup写法isSetupStore=typeofsetup==='function'内部创建useStore函数,并给函数绑定$id属性为用......
  • Vue 2.x源码学习:render方法、模板解析和依赖收集
    众所周知,Vue的脚手架项目是通过编写.vue文件来对应vue里组件,然后.vue文件是通过vue-loader来解析的,下面是我学习组件渲染过程和模板解析中的一些笔记。之前的笔记:应用初......
  • vue2源码学习2vuex&vue-router
    1.vue插件编写插件可以实现对象vue的拓展,比如新增全局属性/方法,添加实例方法,使用mixin混入其他配置项等等。编写插件必须要实现install方法,当调用Vue.use()使用插件时,......
  • 分析Java的类加载器与ClassLoader(二):classpath与查找类字节码的顺序,分析ExtClassLoader
    先回顾一下classpathclasspath的作用:    classpath的作用是指定查找类的路径:当使用java命令执行一个类(类中的main方法)时,会从classpath中进行查找这个类。 指定clas......