redis zskiplist

skiplist 是一种有序数据结构。它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。 跳跃表支持平均O(log N) 最坏 O(N)
复杂度的节点查找。

1、跳表介绍

为了使得链表支持类似二分查找的算法,对原始的链表进行修改,修改后的链表就是跳跃表,简称跳表。跳表支持快速的插入、删除、查找操作,是一种动态的数据结构。

1-1

如果我们想查找图1-1 的链表里面的某个元素,那必须从头遍历到尾才能找到,时间复杂度为O(n)

1-2

我们可以给原始链表加一级索引,如上图1-2,从图中可以看出如果我们此时查找是6这个元素的,从红线表示看我们只需要查找5次,原始链表查询需要6次

1-3

我们可以给原始链表再增加一级索引,来减少我们的查找时间复杂度。

由于这个数据量不大,查找效率提升的不明显,对于数据量较大的时候,查找效率提高很快,拿空间换时间。
JAVA中也有跳表的实现-ConcurrentSkipListMap。

2、redis skiplist 结构

在redis中 对外暴露的数据结构:sorted set,然后sorted set 底端不仅仅使用skiplist,还是使用ziplist和dict (关于ziplist 后面章节会介绍),那么具体什么情况下面才会使用skiplist

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
  zset-max-ziplist-entries 128
  zset-max-ziplist-value 64

通过redis.conf 上面这俩个配置来具体确定 ziplist 到skiplist的转换

  • zset-max-ziplist-entries :当sorted set中的元素个数,即(数据, score)对的数目超过128的时。
  • zset-max-ziplist-value :当sorted set中插入的任意一个数据的长度超过了64的时。
1-1
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

上面代码出自redis-4.0 的 server.h ,对上面结构做些简单分析

  • ZSKIPLIST_MAXLEVEL:创建zskiplist会创建一个空的zskiplistNode (zskiplist.header的指向) 该node.level[] 的长度即该参数,还有就是每次创建新node的时会随机数字,该随机数范围最大值就是该参数。生成的随机数为node.level[]的长度
  • ZSKIPLIST_P:生成随机数概率期望
/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
  • zskiplistNode定义了skiplist节点结构。
    ele : 存储数据 为sds 类型(关于sds 可参考前面章节)
    score :数据对应的分数
    *backward:指向位于当前节点的前一个节点(双向链表提升遍历时间复杂度)
    level :节点中用 L1 、 L2 、 L3 等字样标记节点的各个层(参考图1-1)存放指向各层链表后一个节点的指针 。forward表示每层的一个向后指针。span 表示记录两个节点之间的距离,可以用于计算元素排名(rank)。
    如图1-1 我们需要找到o3这个元素 对应的score为2.0,从header开始找到level最大一个元素 即o3,o3.span 为3 ,在根据o3 的backward 找到o2,o3到o2的span为1,那么计算排名则为(3-1)-1 =1 ,rank是从0开始计算,所以从图中可以看出o2的rank。这也是redis对skiplist做的一层扩展,并且level[] 是一个柔性数组。
  • zskiplist 定义了skiplist真正结构
    header和tail 分别指向skiplist 的头尾节点。
    length :记录所有的节点数,其中不包含新创建zskiplist时 创建的空node。
    level :记录所有节点 zskiplistNode.level[] 长度最大的数字,其中也不计算新创建zskiplist时 创建的空node(这个node的level[] 长度为32)

3、redis skiplist 创建过程以及skiplist的转换

创建过程

为什么把创建过程单独拿出来作为一项,因为笔者在刚刚开始的时候对跳表完全不了解,看了很多资料也还是一知半解。特别当笔者第一次看到图1-1(别人画的借用下)完全不理解header指向为什么是一个null的node 而且还是长度为32的level[],所以这里面做下特殊的源码讲解。

/* Create a skiplist node with the specified number of levels.
 * The SDS string 'ele' is referenced by the node after the call. */
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}

上述代码出自redis-4.0 的 t_zset.c ,简单做下分析

  • zslCreate 创建skiplist 并且创建header 的node,可以看出来传过去的参数为ZSKIPLIST_MAXLEVEL,直接创建一个长度为32的level[],看到这里就很清楚上图1-1 上面所画的。
  • zslCreateNode 创建具体node节点。

下面分析插入节点操作:

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    serverAssert(!isnan(score));
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    level = zslRandomLevel();
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

代码很长,总体思路大概分为3步:

  • 根据目前传入的score找到插入位置x,这个过程会保存各层x的前一个位置节点。
  • 根据随机函数获取level,并且生成新节点。
  • 修改各个指针的指向,将创建的新节点插入。
skiplist的转换

前面介绍sorted set并不是一开始就用skiplist 而是后面才开始转换成的。通过上面介绍的zset-max-ziplist-entries 和zset-max-ziplist-value来控制的。

  • 当数据量不满足zset-max-ziplist-entries 和zset-max-ziplist-value时,sorted set是由一个ziplist来实现的。
  • 当满足zset-max-ziplist-entries 和zset-max-ziplist-value其中之一,sorted set会通zset 的数据结构来实现转换skiplist
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

4、为什么redis用skiplist而不用平衡树

关于这个话我们可以从查找、新增删除的角度考虑

平衡树:采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度。

  • 查找:在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。然而用skiplist 进行范围查找的时候,我们只需要找到小值在对第一层进行遍历即可得到;查找单个key 时平衡树和skiplist 时间复杂度基本差不多都为O(log n)
  • 新增删除:平衡树新增和删除可能会引起子树的调整,而skiplist 只需要改变向前指针或者向后指针的改变,操作简单快速。

从上面三点其实已经说明很多选择skiplist的优势,还有一点skiplist 实现起来比平衡树简单很多。

Redis作者:(原话地址

There are a few reasons:

  1. They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
  2. A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
  3. They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

5、总结

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

推荐阅读更多精彩内容