重现Redis--数据结构与对象(二)

本文深入探讨Redis中的跳表数据结构,介绍了跳表的基本概念、理论背景及其在Redis中的具体实现方式。文章详细分析了跳表的创建、节点插入与删除等关键操作,并提供了一个简单的个人实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

重现Redis–数据结构与对象(二)


前言

感想

个人觉得阅读源码是一种非常快的学习方法:
1.可以吸收到非常优秀的设计模式和规范的代码书写。
2.源码中有许多API,可以用来一边阅读一边写测试代码;这样做其实一方面保持在C/C++上的编程熟练度,另一方面这种做法也是一种现学现用习惯的培养。
写下这个系列,一方面在于一种形式化的行为让自己坚持细心的阅读源码,另一方面也希望把自己的收获带给其他有共同爱好的朋友~

关于本文

前面文章我们讨论了SDS、链表、字典的实现等,这篇文章我们将会关注跳表、整数集合、压缩列表这三个数据结构的实现源码。
下面,我们就来看看Redis是怎么设计处理这三种数据结构的~


跳表(zskiplist)

跳表结构的定义放在server.h中,API函数的实现均放在t_zset.c中。
跳表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针达到快速访问节点的目的。跳表增删查改效率平均都是O(nlogn),支持平均O(logn)、最坏O(n)的节点查找,大部分情况下,跳表效率可以和平衡树媲美,而且实现还相对简单。
Redis里面只有两个地方用到了跳表,一个是有序集合键,另一个是在集群中用作内部数据结构。

跳表理论知识

当谈到有序的数据结构的实现时,平衡树无疑是一种高效而且流行的选择。在平衡树里面最出名的AVL和红黑树这些也广泛被应用:例如STL里的set与map就由红黑树作为底层数据结构。关于平衡树的具体理论可以参考:http://blog.youkuaiyun.com/lemonoil/article/details/54405613
平衡树的缺点也是非常明显的:实现太复杂!光是旋转操作左旋右旋左右旋……一般人还真记不住。跳表这种数据结构可以帮助我们在大部分情况下简化操作。
如果下次有人问你:如何让链表的元素查询接近线性时间?你可以用跳表这个数据结构好好的秀一番知识存储~

下面简单转述一下跳表的理论(原文可以戳:http://www.cnblogs.com/acfox/p/3688607.html):

引入

普通单链表查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时间复杂度。
这里写图片描述
既然容易想到二分的思想去缩短查询时间,那么增加空间消耗就可以达到这一点:
这里写图片描述
如何做到更快呢?
这里写图片描述
很容易发现这种思想和二分有异曲同工之妙,最后我们得到的结构是:
这里写图片描述
直觉上认为,这样的结构会让查询有序链表的某个元素更快。那么究竟算法复杂度是多少呢?
如果有n个元素,因为是2分,所以层数就应该是log n层 (本文所有log都是以2为底),再加上自身的1层。以上图为例,如果是4个元素,那么分层为L3和L4,再加上本身的L2,一共3层;如果是8个元素,那么就是3+1层。最耗时间的查询自然是访问所有层数,耗时logn+logn,即2logn。为什么是2倍的logn呢?我们以上图中的46为例,查询到46要访问所有的分层,每个分层都要访问2个元素,中间元素和最后一个元素。
所以时间复杂度为O(logn)。
在这就是最理想的跳表实现,不过缺点也很明显:调整一个理想的跳表将是一个比调整平衡二叉树还复杂的操作。
幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。

容易实现的跳表

允许简单的插入和删除元素,并提供O(logn)的查询时间复杂度。
先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
在这里每一次插入过程其实就是一次抛硬币,在每一层插入时候都会抛一次硬币看看是否在该层插入。
以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的都知道,最终的表结构肯定非常接近于理想跳跃表。

更多细节与一种C++实现

http://blog.youkuaiyun.com/yinlili2010/article/details/39503655
这篇博客写的非常不错,建议先看看,理清整个算法后再看Redis实现将会很轻松。

zskiplist的定义

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    // 在老的版本中出现的是 robj *obj;
    // 其本质对象是指向了一个字符串对象,底层也是SDS实现的
    sds ele;
    // 用于排序的分值
    double score;
    // 后退指针,指向最大的比自己小的节点
    struct zskiplistNode *backward;
    // 这个和sds一样,struct结尾的[]数组表示之后struct长度不定,之后的数据地址起始都由level表示
    // 记录level[i]这一层forward指针指向下一个,中间跳过了多少个元素
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    // 跳表节点数
    unsigned long length;
    // 表中层数最大的节点层数
    int level;
} zskiplist;

zskiplistNode用来表示跳跃表节点,而zskiplist保存了一些关于跳跃表的信息。
在zskiplist里面包含这些信息:
-header,tail:表头表尾指针,用于常数时间内定位表头表尾
-length:跳表目前拥有的节点数(注意:表头节点并不在计算中,这种计算方法比较像链表拥有一个表头节点一个道理),常数时间内定位跳表长度信息
-level:层数最大的那个节点的层数(注意:表头节点也不在计算中)

在zskiplistNode里面包含这些信息:
1.分值和成员:
score是一个double的浮点数,用来进行排序
ele是一个SDS对象,每个节点的SDS对象必须是唯一的,但是多个节点的score可以是一样的:分值相同的节点由SDS成员对象的字典序来排序
2.后退指针:
backward 指向跳跃表的前一个节点
3.层
level[] 这个属性至关重要,是跳跃表的核心所在,初始化一个跳跃表节点的时候会根据幂次定律(越大的数出现的概率越小)为其随机生成一个层大小(介于1和32之间),每个节点的每一层以链表的形式连接起来。一般而言,层数越多访问其他节点速度越快。
其中表头默认是32层。
这里写图片描述
(纠正一下上图,length应该是4)

API

这里写图片描述

实现分析

创建跳表
/* 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在创建一个跳跃表的时候完成以下操作:
创建一个zskiplist结构
设定其level为1,长度length为0
初始化一个表头结点,其层数为32层,每一层均指向NULL

插入节点

在介绍插入的算法前有个函数值得一提,这个函数负责随机生成1-32层数的一个节点:

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

不难想到,往跳跃表中插入一个节点,必然会改变跳表的长度,可能会改变其长度。而且对于插入位置处的前后节点的backward和forward指针均要改变。
插入节点的关键在找到在何处插入该节点,跳跃表是按照score分值进行排序的,其查找步骤大致是:从当前最高的level开始,向前查找,如果当前节点的score小于插入节点的score,继续向前;反之,则降低一层继续查找,直到第一层为止。此时,插入点就位于找到的节点之后。

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    // updata[]数组记录每一层位于插入节点的前一个节点
    // 这种可以参考链表的插入
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    // rank[]记录每一层位于插入节点的前一个节点的排名
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    // 从表头节点的最高层开始查找
    for (i = zsl->level-1; i >= 0; i--) {
        // 存储rank值是为了交叉快速地到达插入位置
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 这里条件稍微多一点 但是基本思路和单链表差不多
        // 前向指针不为空,前置指针的分值小于score或当前向指针的分值等
        // 于空但成员对象不等于o的情况下,继续向前查找
        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;
    }

    level = zslRandomLevel();
    if (level > zsl->level) {
        // 如果level大于当前存储的最大level值
        // 设定rank数组中大于原level层以上的值为0
        // 同时设定update数组大于原level层以上的数据
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        // 更改记录的最大level
        zsl->level = level;
    }

    // 创建插入节点
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        // 针对跳跃表的每一层,改变其forward指针的指向
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        // 更新插入节点的span值
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        // 更新插入点的前一个节点的span值
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    // 更新高层的span值
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    // 设定插入节点的backward指针
    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;
}

简单说一下,其实在跳表看似复杂的插入操作里面,最根本的思想还是源于单链表的插入操作~

删除节点

Redis提供了三种跳跃表节点删除操作。
分别如下:
-根据给定分值和成员来删除节点,由zslDelete函数实现
-根据给定分值来删除节点,由zslDeleteByScore函数实现
-根据给定排名来删除节点,由zslDeleteByRank函数实现
上述三种操作的删除节点部分都由zslDeleteNode函数完成。

zslDeleteNode函数用于删除某个节点,需要给定当前节点和每一层下当前节点的前一个节点。

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {
            // 如果x存在于该层,则需要修改前一个节点的前向指针
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            // 反之,则只需要将span-1
            update[i]->level[i].span -= 1;
        }
    }
    // 修改backward指针,需要考虑x是否为尾节点
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    // 如果被删除的节点为当前层数最多的节点,
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

有了zslDeleteNode后,三种删除函数十分容易,在这里就不贴上分析了。

获取给定分值和成员的节点的排名

跳跃表获取排名的平均复杂度为O(logN),最坏为O(n)。
其实现如下:

unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;

    x = zsl->header;
    // 从最高层开始查询
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                compareStringObjects(x->level[i].forward->obj,o) <= 0))) {
            // 前向指针不为空,前置指针的分值小于score或当前向指针的
            // 分值等于空但成员对象不等于o的情况下,继续向前查找
            rank += x->level[i].span;
            x = x->level[i].forward;
        }

        // 此时x可能是header,所以此处需要判断一下
        if (x->obj && equalStringObjects(x->obj,o)) {
            return rank;
        }
    }
    return 0;
}

直观来看就是和跳表理论里面思路一样:
这里写图片描述
注意到这个循环在插入中也出现过,可以说算是利用这个数据结构核心思想的代码~

个人实现

下面是一种个人的简单实现:

#include <iostream>
#include <cstdlib>

#define MAXLEVEL 32
#define INF 0x3fff

using namespace std;

namespace LJH{
    class skiplistNode{
    public:
        int value;
        skiplistNode *forward[MAXLEVEL];

        skiplistNode(){
            value = -1;
            for(int i = 0;i < MAXLEVEL;i++)
                forward[i] = NULL;
        }
        ~skiplistNode(){

        }
        skiplistNode& operator = (const skiplistNode* &node){
            value = node->value;
            for(int i = 0;i < MAXLEVEL;i++){
                forward[i] = node->forward[i];
            }
            return *this;
        }
    };

    class skiplist{
    public:
        skiplistNode *header;
        int listLevel;

        skiplist(){
            header = new skiplistNode;
            listLevel = 0;
            header->value = -INF;
            skiplistNode *first = new skiplistNode;
            first->value = -INF;
            skiplistNode *end = new skiplistNode;
            end->value = INF;
            for(int i = 0;i < MAXLEVEL;i++){
                header->forward[i] = first;
                header->forward[i]->forward[i] = end;
            }
        }
        ~skiplist(){
            delete header;
        }

        int randomLevel();
        int insert(int value);
        skiplistNode* search(int value);
        void printList();
        int deleteNode(int value);
    };

    int skiplist::randomLevel()
    {
        int upcount = 0;
        for(int i=0;i<MAXLEVEL;i++)
        {
            int num = rand()%10;
            if(num<5)
            {
                upcount++;
            }
        }
        return upcount;
    }

    void skiplist::printList() {
        skiplistNode *current = header;
        for(int i = listLevel-1;i >= 0;i--){
            cout << "level " << i << ".................." << endl;
            while(current->forward[i]){
                cout << " " << current->value;
                current = current->forward[i];
            }
            cout << " " << current->value << endl;
        }
    }

    int skiplist::insert(int value) {
        skiplistNode *node = new skiplistNode;
        int newLevel = randomLevel();
        node->value = value;

        skiplistNode *update[MAXLEVEL];
        for(int i = 0;i < MAXLEVEL;i++){
            update[i] = header->forward[i];
        }
        skiplistNode *current = new skiplistNode;
        current = header;
        skiplistNode *last = new skiplistNode;
        last = header;

        for(int i = listLevel-1;i >= 0;i--){
            while(current->forward[i] && current->forward[i]->value != INF && current->forward[i]->value < value){
                current = current->forward[i];
            }
            // store the last node of every level
            update[i] = current;
        }
        last = current->forward[0];

        if(last != NULL && last->value == value){
            cout << "inset key: "<< value << " already existed"<<endl;
            return 0;
        }

        if(newLevel > listLevel)
            listLevel = newLevel;

        for(int k = 0; k < listLevel;k++)
        {
            // update forward pointer
            node->forward[k] = update[k]->forward[k];
            update[k]->forward[k] = node;
        }
        return 1;
    }

    skiplistNode* skiplist::search(int value) {
        skiplistNode *current = new skiplistNode;
        current = header;
        for(int i = listLevel-1;i >= 0;i--){
            while(current->forward[i] && current->forward[i]->value != INF && current->forward[i]->value < value){
                current = current->forward[i];
            }
        }
        current = current->forward[0];
        if(current != NULL && current->value == value){
            cout << "find " << value <<endl;
            return current;
        } else{
            cout << "cannot find " << value <<endl;
            return NULL;
        }
    }

    int skiplist::deleteNode(int value) {
        skiplistNode *update[MAXLEVEL];
        for(int i = 0;i < MAXLEVEL;i++){
            update[i] = header->forward[i];
        }

        skiplistNode* current = new skiplistNode;
        current = header;
        skiplistNode* last = new skiplistNode;
        current = header;

        for(int i = listLevel-1;i >= 0;i--){
            while(current->forward[i] && current->forward[i]->value != INF && current->forward[i]->value < value){
                current = current->forward[i];
            }
            // store the last node of every level
            update[i] = current;
        }
        last = current->forward[0];

        if(last->value != value)
        {
            cout << "delete key: " << value << " does not existed"<<endl;
            return 0;
        }
        for(int i = 0; i < listLevel;i++)
        {
            update[i]->forward[i] = update[i]->forward[i]->forward[i];
        }
        return 1;
    }
}

这里写图片描述
这里写图片描述
这里写图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值