leveldb 源码分析 —— SkipList跳表

leveldb 源码分析 —— SkipList跳表

原文

leveldb 存取数据,都在用 MemTable 这个结构体,而 MemTable 核心在于 level::MemTable::Table,也就是 typedef SkipList<const char*, KeyComparator> level::MemTable::Table。 SkipList 看名字就知道,跳表,是一种数据结构,允许快速查询一个有序连续元素的数据链表。这是一种 "以空间换取时间" 的一种做法,值得注意的是,这些链表都是有序的

关于这个跳表,我查了一下作者(William Pugh)给出的解析:

Skip lists are a data structure that can be used in place of balanced trees. Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.

跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的 插入和删除的工作是比较简单的。

也就是说核心在于随机算法,一个靠谱的随机算法对跳表是非常重要的。

现在我们来一边用代码加图解来分析一下跳表魅力!

跳表数据存储模型

跳表数据结构如下:

template <typename Key, typename Value>
class SkipList {
private:
    struct Node; // 声明节点结构体
    
public:
  explicit SkipList(); 

private:
  int level_;  // 跳表层数
  Node* head_; // 跳表头部节点列表
  unit32_t rnd_; // 随机数因子
  
  // 生成节点方法
  Node* NewNode(int level, const Key& key,  const Value& value);
 
  Node* FindGreaterOrEqual(const Key& key, Node** prev) const;
  
};

节点数据结构:

template <typename Key, typename Value>
struct SkipList<Key, Value>::Node {

  explicit Node(const Key& k, const Value& v) : key(k), value(v) {}

  Key key;  
  Value value;
  
  void SetNext(int i, Node* x);  
  Node* Next(int i);
  
private:
  struct Node* forward_[1]; // 节点数组列表,这个比较重要,后面会详细介绍,如果不理解这个变量,就很难理解跳表了
}

通过图来看一下总体结构

[图片上传失败...(image-c31a3f-1523849207450)]

ps:图中虚线链接表述数组关系,实现标识指针链表关系

上图假设 level 为 4 等级的一个跳表图,,forward_ 变量是一个指针数组,一边指向下一个节点(黑色填充箭头)的链表,一边又是这些链表的数组(透明填充箭头)这样的一个数据结构形成了我们需要的一个链表。

后面我们会称为图中竖向(dowm)节点为节点数组,横向(left)的节点为节点链表

初始化跳表

首先为了实现这个结构,我们来初始化跳表

enum {kMaxLevel = 12}; // 这里初始化默认跳表最大层数高度为12

template <typename Key, typename Value>
SkipList<Key, Value>::SkipList() : head_(NewNode( kMaxLevel, 0, 0)), rnd_(0xdeadbeef)
{
  // 将 head 节点数组全部初始化
  for (int i = 0; i < kMaxLevel; ++i) {
    head_->SetNext(i, nullptr); // 设置第 i 层节点
  }
}

现在我们的结构就实现了下图的样子了

[图片上传失败...(image-dee636-1523849207450)]

当然,这些节点都是空的就是了。NewNode 方法查看

插入操作

插入操作分为两步:

  1. 查找每层链表,知道找到该插入的位置(因为要保持有序的)
  2. 更新节点指针和跳表高度

第一步:

template <typename Key, typename Value>
typename SkipList<Key, Value>::Node*
SkipList<Key, Value>::FindGreaterOrEqual(const Key& key, Node** prev) const
{
  Node* x = head_, *next = nullptr;

  int level = level_ - 1;
  
  // 从最高层往下查找需要插入的位置
  // 填充 prev,prev 为用来记录每 层(level)跳点的位置
  for (int i = level; i >= 0 ; --i) {
    while ( (next = x->Next(i)) && next->key < key) {
      x = next;
    }
    if (NULL != prev) {prev[i] = x;}
  }
  return next;  // 返回第 level0 层最合适插入的节点位置
};

第一步操作如图3.1 所示, 往这一跳表中插入 key=17 的操作, 可以看出跳表不断寻找跳点,记录跳点 (红色框框住的点,我们以下称为跳点),寻找该插入的位置,例如 图3.1中运行上面代码后,返回了 next 为 key=12 的节点,因为 key=17 大于 12 ,小于 19。

图 3.1

[图片上传失败...(image-b179bb-1523849207450)]

第二步:

template <typename Key, typename Value>
bool SkipList<Key, Value>::Insert(const Key& key, const Value& value) {
  
  /** 第一步实现*/  
  // prev 为用来记录每 层(level)跳点的位置
  Node* prev[kMaxLevel];

  // 查找每层链表,知道找到该插入的位置(因为要保持有序的)
  Node* next = FindGreaterOrEqual(key, prev);
    
  int level;    
  
  // 不能插入相同的key
  if ( next && next->key == key ) {
    return false;
  }
  
  /** 第二部实现, 第二步实现后的代码如图 3.2*/
  // 产生一个随机层数 k
  level = randomLevel();
  if (level > level_) {
    for (int i = level_; i < level; ++i) {
      prev[i] = head_; // 新增的层数初始化
    }
    level_ = level;
  }
  
  // 新建一个待插入节点 next,
  next = NewNode(level, key, value);
  // 逐层更新节点的指针, 一层一层插入
  for (int j = 0; j < level; ++j) {
    next->SetNext(j, prev[j]->Next(j)); // 该节点第 levelJ 层的节点指向 prev (跳点位置)的 levelJ 层链表指向的节点
    prev[j]->SetNext(j, next); // 将 pre 跳点第 levelJ 层链表指向了 Next 第 levelJ 层的链表节点
  }
  
    return true;
}

上述代码中 randomLevel() 生成的层数,就作为了跳表的总层数,同时,也代表了这个新增节点的层数,例如 图3.2 中,节点 key=3,高度为1,key=6,高度为4。

图 3.2
[图片上传失败...(image-907dc9-1523849207450)]

randomLevel 随机层数生成
setNext 设置节点链表

查找操作

插入操作中的第一步就是我们的查找操作了,就不做解析了,直接封装一层代码

template <typename Key, typename Value>
Value
SkipList<Key, Value>::Find(const Key &key) {
  Node* node = FindGreaterOrEqual(key, NULL);
  if (node) {
    return node->value;
  }
  return NULL;
}
删除操作

在 leveldeb 中,跳表 SkipList 是没有删除操作的,leveldb 的跳表只是用来增加节点个查询节点,如果要删除某个节点,只是将某个节点标记为删除,因为删除操作又得重新计算 level 层数,更新每层的节点链表,这样太耗费性能了。

但是我们在这里还是实现一下跳表的删除操作,同样的,跳表删除和插入操作相同

  1. 首先查找到需要删除的节点
  2. 如果找到该节点,更新指针域,需要更新 level 的话,逐层更新每个链表
template <typename Key, typename Value>
bool
SkipList<Key, Value>::Delete(const Key&key)
{
  Node* prev[kMaxLevel];
  Node* next = FindGreaterOrEqual(key, prev);

  int level = level_;
  if (next && next->key == key) {
    // 将每层跳点链表设置到 next 节点所指向的每层的链表
    for (int i = 0; i < level; ++i) {
      if (prev[i]->Next(i) && prev[i]->Next(i)->key == next->key) {
        prev[i]->SetNext(i, next->Next(i));
      }
    }

    // 释放该节点数组的所有内存
    free(next);

    //如果删除的是最大层的节点,那么需要重新维护跳表的
    for (int j = level_-1; j >= 0 ; --j) {
      if (head_->Next(j) == NULL) {
        level_--;
      }
    }
    return true;
  }

  return false;
};

图4.1
[图片上传失败...(image-590362-1523849207450)]

如 图4.1所示,删除节点 key=17 时候的操作,先查找并返回 next 节点,检查 next 节点是否 key=17,如果是的是,则将逐层的跳点全部更新过来,并更新层数。

附属实现代码
生成节点方法
template <typename Key, typename Value>
typename SkipList<Key, Value>::Node*
SkipList<Key, Value>::NewNode(int level, const Key& key,  const Value& value)
{
  size_t men = sizeof(Node) + level * sizeof(Node*);
  Node* node = (Node*)malloc(men);
  node->key = key;
  node->value = value;
  return node;
}

代码中 sizeof(Node) 为本身结构体所需要的内存分配,level * sizeof(Node*) 是为 forward_ 数组分配内存,因为要配 level 个节点链表。 在 leveldb 中使用了字节对齐的方式来分配这块内存,我这边并没有写出来,有兴趣的可以浏览一下源码。

我们假设 level = 4

图 6.1
[图片上传失败...(image-53155-1523849207450)]

代码生成了图6.1的结构,level0 节点的 forward_ 数组大小为4,leve1 ~ level3 都为空节点,但是分配了 8 个字节的指针内存 (64位操作系统)。图中虚线为数组引用表达,并不是指针指向。

随机层数生成数方法实现

取自google开源项目leveldb的实现

template <typename Key, typename Value>
int SkipList<Key, Value>::randomLevel() {

  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxLevel);
  return height;
}

uint32_t Next( uint32_t& seed) {
  seed = seed & 0x7fffffffu; // 防止负数

  if (seed == 0 || seed == 2147483647L) { 
    seed = 1;
  }

  static const uint32_t M = 2147483647L;   // 2^31-1
  static const uint64_t A = 16807;  // bits 14, 8, 7, 5, 2, 1, 0
  // We are computing
  //       seed_ = (seed_ * A) % M,    where M = 2^31-1
  //
  // seed_ must not be zero or M, or else all subsequent computed values
  // will be zero or M respectively.  For all other values, seed_ will end
  // up cycling through every number in [1,M-1]
  uint64_t product = seed * A;

  // Compute (product % M) using the fact that ((x << 31) % M) == x.
  seed = static_cast<uint32_t>((product >> 31) + (product & M));
  // The first reduction may overflow by 1 bit, so we may need to
  // repeat.  mod == M is not possible; using > allows the faster
  // sign-bit-based test.
  if (seed > M) {
    seed -= M;
  }
  return seed;
}

总体来说这个 level 层数的生成方法也不是随机的,根据 seed 不断被修改的次数来决定层数,换而言之就是 level0 节点数量来决定层数。

有关节点结构体的方法实现
template <typename Key, typename Value>
void 
SkipList<Key, Value>::SetNext(int i, Node* x) {
    assert(i >= 0);
    forward_[i] = x; // 设置数组节点
}

template <typename Key, typename Value>
void 
SkipList<Key, Value>::Node* Next(int i) {
    assert(i >= 0);
    return forward_[i];
}

SetNext(int i, Node* x) 方法是设置 forward_ 节点数组第 i 层(level)的链表引用。
例如图6.1 中,key=10 调用了 SetNext(4, Node where key = 20 and level = 4) 和 key=20 调用了 SetNext(4, Node where key = 40 and level = 4) 的表述。

图 6.2
[图片上传失败...(image-50d9c4-1523849207450)]

Next(int i) 为取出某层节点链表的方法,这个应该不应解析了吧。

输出跳表结构
template <typename Key, typename Value>
void
SkipList<Key, Value>::Print()
{
  Node* next, *x = head_;

  printf("--------\n");
  for (int i = level_ - 1; i >= 0; --i) {
    x = head_;
    while ((next = x->Next(i))) {
      x = next;
      std::cout << "key: " << next->key << " -> ";
    }
    printf("\n");
  }
  printf("--------\n");
}

Print 方法来输出查看当前跳表有哪些节点结构

参考资料:
跳表SkipList
Skip List(跳跃表)原理详解与实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容