5. LevelDB源码剖析之基础部件-SkipList

5.1 基本原理

SkipList称之为跳表,可实现Log(n)级别的插入、删除。跳表是平衡树的一种替代方案,和平衡树不同的是,跳表并不保证严格的“平衡性”,而是采用更为随性的方法:随机平衡算法。
关于SkipList的完整介绍请参见跳表(SkipList),这里借用几幅图做简要说明:

图1.1 跳表

  • 图1.1中红色部分为初始化状态,即head各个level中next节点均为NULL。
  • 跳表是分层的,由下往上分别为1、2、3...,因此需要分层算法。
图1.2 查找、插入
  • 跳表中每一层的数据都是按顺序存储的,因此需要Compactor。
  • 查找动作由最上层开始依序查找,直到找到数据或查找失败。
  • 插入动作仅影响插入位置前后节点,对其他节点无影响。
图1.3 查找、删除
  • 删除动作仅影响插入位置前后节点,对其他节点无影响。

5.2 分层算法

分层算法决定了数据插入的Level,SkipList的平衡性如何全权由分层算法决定。极端情况下,假设SkipList只有Level-0层,SkipList将弱化成自排序List。此时查找、插入、删除的时间复杂度均为O(n),而非O(Log(n))。
LevelDB中的分层算法实现如下(leveldb::skiplist::RandomHeight())

// enum { kMaxHeight = 12 };
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_.Next() % kBranching) == 0)) {
            height++;
        }
        assert(height > 0);
        assert(height <= kMaxHeight);
        return height;
    }

kMaxHeight 代表Skiplist的最大高度,即最多允许存在多少层,为关键参数,与性能直接相关。修改kMaxHeight ,在数值变小时,性能上有明显下降,但当数值增大时,甚至增大到10000时,和默认的kMaxHeight =12相比仍旧无明显差异,内存使用上也是如此。

为何如此?关键在于while循环中的判定条件:height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)。除了kMaxHeight 判定外,(rnd_.Next() % kBranching) == 0)判定使得上层节点的数量约为下层的1/4。那么,当设定MaxHeight=12时,根节点为1时,约可均匀容纳Key的数量为4^11=4194304(约为400W)。因此,当单独增大MaxHeight时,并不会使得SkipList的层级提升。MaxHeight=12为经验值,在百万数据规模时,尤为适用。

5.3 比较器(Compactor)

如同二叉树,Skiplist也是有序的,键值比较需由比较器(Compactor)完成。SkipList对Compactor的要求只有一点:()操作符重载,格式如下:

//a<b返回值小于0,a>b返回值大于0,a==b返回值为0
int operator()(const Key& a, const Key& b) const;

SkipList定义中,Key与Compactor均为模板参数,因而Compactor亦由使用者实现。

template <typename Key, class Comparator>
class SkipList
{
    ....
}

LevelDB中存在一个Compactor抽象类,但该抽象类并没有重载()操作符,至于Compacotr如何使用及Compactor抽象类和此处的Compactor的关系如何请参见MemTable一节。

5.4 查找、插入、删除

LevelDB中实现的SkipList并无删除行为,这由其业务特性决定,故此处不提。
查找、插入亦即读、写行为。由图1.2可知,插入首先需完成一次查找动作,随后在指定位置上完成一次插入行为。

5.4.1 查找

LevelDB中的查找、插入行为几乎做到了“无锁”并发,这一点是非常可取的。关于这一点,是本次备忘的重点。先来看查找:

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)) {
                // Keep searching in this list
                x = next;
            }
            else {
                if (prev != NULL) prev[level] = x;
                if (level == 0) {
                    return next;
                }
                else {
                    // Switch to next list
                    level--;
                }
            }
        }
    }

实现并无特别之处:由最上层开始查找,一直查找到Level-0。找到大于等于指定Key值的数据,如不存在返回NULL。来看SkipList的Node结构:

template<typename Key, class Comparator>
    struct SkipList<Key, Comparator>::Node {
        explicit Node(const Key& k) : key(k) { }

        Key const key;

        // Accessors/mutators for links.  Wrapped in methods so we can
        // add the appropriate barriers as necessary.
        Node* Next(int n) {
            assert(n >= 0);
            // Use an 'acquire load' so that we observe a fully initialized
            // version of the returned Node.
            return reinterpret_cast<Node*>(next_[n].Acquire_Load());
        }
        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].Release_Store(x);
        }

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

    private:
        // Array of length equal to the node height.  next_[0] is lowest level link.
        port::AtomicPointer next_[1];    //看NewNode代码,实际大小为node height
    };

Node有两个成员变量,Key及next_数组。Key当然是节点数据,next_数组(注意其类型为AtomicPointer )则指向了其所在层及之下各个层中的下一个节点(参见图1.1)。Next_数组的实际大小和该节点的height一致,来看Node的工厂方法NewNode:

template<typename Key, class Comparator>
    typename SkipList<Key, Comparator>::Node*
        SkipList<Key, Comparator>::NewNode(const Key& key, int height) 
    {
        //此处分别的为对象体,需要边界对齐
        char* mem = arena_->AllocateAligned( sizeof(Node) + 
                 sizeof(port::AtomicPointer) * (height - 1));

        //c++ placement显示调用构造函数,并不常见。
        return new (mem) Node(key);    
    }

Node的两组方法:SetNext/Next、NoBarrier_SetNext/NoBarrier_Next,用于读写指定层的下一节点指针,前者并发安全、后者非并发安全。

5.4.2 插入

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 == NULL || !Equal(key, x->key));

        int height = RandomHeight();
        if (height > GetMaxHeight()) 
        {
            for (int i = GetMaxHeight(); i < height; i++) {
                prev[i] = head_;
            }
            //fprintf(stderr, "Change height from %d to %d\n", max_height_, height);

            // 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_ (NULL), or a new value set in
            // the loop below.  In the former case the reader will
            // immediately drop to the next level since NULL sorts after all
            // keys.  In the latter case the reader will use the new node.
            max_height_.NoBarrier_Store(reinterpret_cast<void*>(height));
        }

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

插入行为主要修改两类数据:max_height_及所有level中前一节点的next指针。

max_height_没有任何并发保护,关于此处作者注释讲的很清楚:

读线程在读到新的max_height_同时,对应的层级指针(new level pointer from head_)可能是原有的NULL,也有可能是部分更新的层级指针。如果是前者将直接跳到下一level继续查找,如果是后者,新插入的节点将被启用。

随后节点插入方是将无锁并发变为现实:

  1. 首先更新插入节点的next指针,此处无并发问题。
  2. 修改插入位置前一节点的next指针,此处采用SetNext处理并发。
  3. 由最下层向上插入可以保证当前层一旦插入后,其下层已更新完毕并可用。

当然,多个写之间的并发SkipList时非线程安全的,在LevelDB的MemTable中采用了另外的技巧来处理写并发问题。

5.5 总结

SkipList的功能定位和B-Tree类似,但实现更简单。LevelDB选用SkipList一来和SkipList的高效、简洁相关,二来SkipList也仅存在MemTable一种简单应用场景。下一节,让我们聊聊MemTable。


转载请注明:【随安居士】http://www.jianshu.com/p/6624befde844

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

推荐阅读更多精彩内容