Redis源码探究系列—跳表(skiplist)源码实现详解

跳表(Skip List)是Redis有序集合(ZSet)底层实现的核心数据结构之一。本文将从数据结构定义到核心算法实现,系统剖析跳表的设计思想与工程细节。通过深入理解跳表,我们不仅能掌握其高效的O(log n) 查找、插入、删除机制,还能为后续深入分析ZSet的完整实现(如何结合字典与跳表实现双重索引)打下坚实基础。

跳表(Skip List)是一种基于有序链表的概率数据结构,通过在链表上建立多级索引,实现了 O(log n) 的查找、插入和删除。它由 William Pugh 在 1990 年提出,核心思想是:如果每两个节点提取一个到上一级,形成"快车道",就能跳过大量无需访问的节点。

查找时从最高层开始,向右走不动了就往下走,逐层缩小范围。这与二分查找的思想一致,但不需要数组,也不需要平衡操作。

一、Redis 跳表的数据结构定义

1.1 跳表节点 ------ zskiplistNode

c 复制代码
// server.h:796-809
typedef struct zskiplistNode {
    sds ele;                                // 成员对象(SDS字符串)
    double score;                           // 分值,用于排序
    struct zskiplistNode *backward;         // 后退指针,类似双向链表的`prev`,只有一级(Level 0的前驱)
    struct zskiplistLevel {
        struct zskiplistNode *forward;     // 该层的前进指针
        unsigned long span;                // 该层到`forward`节点之间跨越的节点数(不含自身,含forward节点)
    } level[];                             // 柔性数组,每个元素代表一层,包含forward和span
} zskiplistNode;

span的用途 :计算排名。查找时累加各层走过的span,就是节点在有序集合中的排名。这让ZRANK/ZREVRANK可以在O(logn)内完成。

柔性数组level[] :每个节点的层数在创建时随机决定,level数组的大小就是该节点的层数。头节点固定为ZSKIPLIST_MAXLEVEL(64)层。

1.2 跳表 ------ zskiplist

c 复制代码
// server.h:811-816
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // header: 头节点(哨兵节点,不存真实数据,固定64层),tail: 尾节点指针
    unsigned long length;                 // 节点数量(不含头节点)
    int level;                            // 当前跳表的最大层数(不含头节点的层数)
} zskiplist;

1.3 关键常量

c 复制代码
// server.h:345-346
#define ZSKIPLIST_MAXLEVEL 64
#define ZSKIPLIST_P 0.25
  • ZSKIPLIST_MAXLEVEL = 64:最大层数。2^64,足以覆盖任何实际数据量
  • ZSKIPLIST_P = 0.25:每个节点晋升到上一层的概率为1/4。这意味着约1/4的节点有第2层,1/16的节点有第3层,以此类推

1.4 内存布局

二、随机层数 ------ zslRandomLevel

c 复制代码
// t_zset.c:55-60
int zslRandomLevel(void) {
    int level = 1; // 初始层数为1(最底层)
    // 随机决定是否晋升到更高层,每晋升一层概率为ZSKIPLIST_P(0.25)
    while ((random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF))
        level += 1; // 满足概率条件则层数加1,直到不满足或达到最大层数
    // 层数不能超过ZSKIPLIST_MAXLEVEL(64),否则截断为最大层数
    return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

这是跳表的核心概率机制:

  1. 从第1层开始
  2. 每次产生一个随机数,如果小于ZSKIPLIST_P * 0xFFFF(约1/4),层数加1
  3. 重复直到随机数不符合条件或达到最大层数

各层节点数期望

层数 期望节点数(相对总量 N)
1 N
2 N/4
3 N/16
4 N/64
... ...
k N/4^(k-1)

为什么选P=0.25而不是P=0.5?Pugh论文中P=0.5是经典选择。Redis选择P=0.25的原因是减少内存占用:

P值 平均层数/节点 查找复杂度 内存开销
0.5 2.0 O(log₂N) 基准
0.25 1.33 O(log₄N) -33%

P=0.25时每个节点平均只有1.33层,比P=0.5的2层节省了33%的内存,而查找复杂度仍然是对数级(底数从2变为4,常数因子略增,但实际差异很小)。相关的计算感兴趣的可以阅读一下之前写的这篇文章:《Redis探究之跳表》

三、创建跳表 ------ zslCreate

c 复制代码
// t_zset.c:62-81
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));                              // 分配跳表结构体内存
    zsl->level = 1;                                           // 初始化跳表层数为1(初始时只有头节点,无真实节点)
    zsl->length = 0;                                          // 初始化节点数为0(不含头节点)
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);  // 创建头节点(哨兵节点),固定64层,score=0,ele=NULL

    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;                 // 初始化头节点每层的前进指针为NULL(表示该层为空)
        zsl->header->level[j].span = 0;                       // 初始化头节点每层的跨度为0(头节点到NULL的距离为0)
    }
    zsl->header->backward = NULL;                             // 头节点的后退指针为NULL(头节点没有前驱)
    zsl->tail = NULL;                                         // 初始化尾指针为NULL(跳表为空)
    return zsl;
}

头节点始终为64层,所有层的前进指针初始化为NULLspan初始化为0。头节点不计入length

四、插入节点 ------ zslInsert

这是跳表最复杂的操作,也是理解跳表的关键。

c 复制代码
// t_zset.c:155-231
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;           // update[i]:记录每一层插入位置的前驱节点
    unsigned long rank[ZSKIPLIST_MAXLEVEL];                  // rank[i]:记录每一层前驱节点距离头节点的跨度
    int i, level;

    x = zsl->header;                                         // 从头节点开始查找插入位置
    // 从最高层(zsl->level-1)到第0层,逐层查找插入点
    for (i = zsl->level-1; i >= 0; i--) {
        rank[i] = (i == (zsl->level-1)) ? 0 : rank[i+1];     // rank[i]初始为上一层的rank,最高层为0
        // 在第i层向右查找,直到找到第一个score大于等于目标score的节点,
        // 或score相等但ele字典序大于等于目标ele的节点
        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;                          // 沿forward指针向右移动
        }
        update[i] = x;                                       // 记录每层查找停止的前驱节点
    }

4.1 查找插入位置

update[]数组 :记录每一层中,插入位置的前驱节点。插入后,新节点在每一层都要插在update[i]之后。

rank[]数组 :记录每一层中,update[i]距离头节点的跨度。用于后续计算span

查找过程(从最高层往下):

  1. 从头节点开始,向右走(forward),条件是forward节点的score更小,或score相同但ele字典序更小
  2. 走不动了就往下走(i--
  3. 记录每层停下的节点到update[i]
  4. 同时累加rank[i]

4.2 随机层数与扩层

c 复制代码
    level = zslRandomLevel();                          // 随机生成新节点的层数(1~64,期望1.33)
    if (level > zsl->level) {                          // 如果新节点层数超过当前跳表最大层数,需要扩展跳表
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;                               // 新增的高层rank初始化为0(头节点)
            update[i] = zsl->header;                   // 新层的前驱节点都是头节点
            update[i]->level[i].span = zsl->length;    // 新层的span为当前跳表长度(头节点到尾节点的跨度)
        }
        zsl->level = level;                            // 更新跳表的最大层数
    }

如果新节点的层数超过了当前跳表的最大层数,需要:

  1. 初始化新层的update为头节点
  2. 头节点新层的span设为跳表总长度(因为头节点到尾节点跨越所有节点)
  3. 更新跳表的level

4.3 创建节点并插入

c 复制代码
    x = zslCreateNode(level,score,ele);                      // 创建新节点,层数为level,score/ele为参数
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;    // 新节点的forward指向前驱的forward(即原本的后继)
        update[i]->level[i].forward = x;                      // 前驱的forward指向新节点

        // 新节点的span = 前驱原span - (新节点与前驱之间的节点数)
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        // 前驱的span = 新节点与前驱之间的节点数 + 1(即新节点本身)
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

span计算解析

  • rank[0]是插入位置在Level 0的排名(距头节点的节点数)
  • rank[i]update[i]在Level i的排名
  • rank[0] - rank[i]update[i]到插入位置之间的节点数

新节点的spanupdate[i]原来的span减去update[i]到新节点之间的节点数

复制代码
update[i]原来的span = update[i]到forward的距离
update[i]到新节点的距离 = rank[0] - rank[i] + 1
新节点的span = update[i]原来的span - (rank[0] - rank[i])

update[i]的新span :就是update[i]到新节点的距离

复制代码
update[i]的新span = rank[0] - rank[i] + 1

4.4 处理超出新节点层数的层

c 复制代码
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

新节点没有这些层,所以update[i]forward的距离增加了1(多跨了一个节点)。

4.5 设置后退指针和尾指针

c 复制代码
    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;
}
  • backward指向Level 0的前驱节点
  • 如果新节点是尾节点(level[0].forward == NULL),更新tail

五、删除节点 ------ zslDeleteNode

c 复制代码
// t_zset.c:234-257
// 删除跳表中的节点x,并维护各层指针和span
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    // 遍历每一层,更新前驱节点的forward和span
    for (i = 0; i < zsl->level; i++) {
        // 如果该层前驱节点的forward正好指向x,说明x在该层存在
        if (update[i]->level[i].forward == x) {
            // 合并span:前驱的span加上x的span再减1(跨过x,直接连到x的后继)
            update[i]->level[i].span += x->level[i].span - 1;
            // 前驱的forward指向x的后继
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            // 如果该层没有x,只需把span减1(下层有x,整体长度减少)
            update[i]->level[i].span -= 1;
        }
    }
    // 维护Level 0的backward指针和跳表tail指针
    if (x->level[0].forward) {
        // 如果x不是最后一个节点,后继的backward指向x的前驱
        x->level[0].forward->backward = x->backward;
    } else {
        // 如果x是最后一个节点,更新跳表的tail指针
        zsl->tail = x->backward;
    }
    // 如果最高层已经没有节点,降低跳表层数
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    // 跳表节点数减1
    zsl->length--;
}

删除比插入简单:

  1. 更新span和forward :遍历每一层,如果update[i]在该层与x相邻(forward == x),则合并span;否则只减1
  2. 更新backward :后继节点的backward指向x的前驱
  3. 收缩level:如果最高层变空,降低跳表层数

5.1 查找并删除 ------ zslDelete

c 复制代码
// t_zset.c:259-286
// 删除跳表中指定score和ele的节点,成功返回1,否则返回0
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    x = zsl->header; // 从头节点开始
    // 从最高层往下,逐层查找目标节点的前驱节点,记录到update[]
    for (i = zsl->level-1; i >= 0; i--) {
        // 在第i层不断向右走,直到下一个节点的score大于目标score,
        // 或score相等但ele字典序大于等于目标ele为止
        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)))
        {
            x = x->level[i].forward;
        }
        update[i] = x; // 记录每层查找停止的前驱节点
    }
    // 到Level 0后,前进到第一个可能匹配的节点
    x = x->level[0].forward;
    // 检查score和ele是否都匹配,只有完全匹配才删除
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
        zslDeleteNode(zsl, x, update); // 调用辅助函数删除节点并维护指针
        if (!node)
            zslFreeNode(x); // 如果不需要返回被删节点,直接释放内存
        else
            *node = x; // 否则返回被删节点指针
        return 1; // 删除成功
    }
    return 0; // 未找到目标节点,删除失败
}

先查找再删除。只有当score和ele都匹配时才删除------因为不同成员可能有相同的score。

六、更新分数 ------ zslUpdateScore

c 复制代码
// t_zset.c:333-376
// 更新跳表中指定节点的分数,如果位置不变则直接赋值,否则删除后重新插入
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    x = zsl->header; // 从头节点开始
    // 从最高层往下,逐层查找目标节点的前驱节点,记录到update[]
    for (i = zsl->level-1; i >= 0; i--) {
        // 在第i层不断向右走,直到下一个节点的score大于目标curscore,
        // 或score相等但ele字典序大于等于目标ele为止
        while (x->level[i].forward &&
            (x->level[i].forward->score < curscore ||
                (x->level[i].forward->score == curscore &&
                 sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x; // 记录每层查找停止的前驱节点
    }

    // 到Level 0后,前进到目标节点
    x = x->level[0].forward;
    // 断言找到的节点必须和参数完全匹配
    serverAssertWithInfo(NULL,x,x->score == curscore && sdscmp(x->ele,ele) == 0);

    // 如果新分数不会影响节点在跳表中的相对位置(前驱<newscore<后继),直接赋值即可
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    {
        x->score = newscore;
        return x;
    }

    // 否则,先删除节点,再以新分数重新插入
    zslDeleteNode(zsl, x, update);
    zskiplistNode *newnode = zslInsert(zsl, newscore, ele);
    return newnode;
}

优化判断:如果新分数不影响节点在跳表中的位置(前驱的score仍小于新score,后继的score仍大于新score),直接修改score即可,O(1)。

否则,先删除再插入,O(logn)。这是一个很实用的优化------很多ZINCRBY操作只改变了微小分数,位置不变的概率很高。

七、排名查询 ------ zslGetRank

c 复制代码
// t_zset.c:440-463
// 计算指定元素在跳表中的排名(1-based),不存在则返回0
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;

    x = zsl->header; // 从头节点开始
    // 从最高层往下逐层查找目标节点
    for (i = zsl->level-1; i >= 0; i--) {
        // 在第i层不断向右走,直到下一个节点的score大于目标score,
        // 或score相等但ele字典序大于目标ele为止
        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)))
        {
            // 每向右走一步,累加该层的span(即跨越的节点数)
            rank += x->level[i].span;
            x = x->level[i].forward;
        }
        // 如果当前节点就是目标节点,返回累计的rank
        if (x->ele && sdscmp(x->ele,ele) == 0) {
            return rank;
        }
    }
    // 没找到目标节点,返回0
    return 0;
}

从最高层开始,向右走时累加span,找到目标节点时返回累加值。O(logn)。

span的存在让排名查询无需遍历Level 0的所有前驱节点,这是跳表相比普通有序链表的关键优势。

八、范围查询 ------ zslFirstInRange / zslLastInRange

c 复制代码
// t_zset.c:378-430
// 查找跳表中第一个在指定范围内的节点(score >= min 且 score <= max)
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
    zskiplistNode *x;
    int i;

    // 先判断整个跳表是否与范围有交集,无交集直接返回NULL
    if (!zslIsInRange(zsl,range)) return NULL;

    x = zsl->header; // 从头节点开始
    // 从最高层往下,逐层定位第一个score >= min的节点
    for (i = zsl->level-1; i >= 0; i--) {
        // 在第i层不断向右走,直到下一个节点的score >= min(即满足范围下界)
        while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score,range))
            x = x->level[i].forward;
    }
    // 到达Level 0后,前进到第一个可能在范围内的节点
    x = x->level[0].forward;
    serverAssert(x != NULL); // 理论上此时x必不为NULL

    // 检查该节点是否超过范围上界(score > max),超出则返回NULL
    if (!zslValueLteMax(x->score,range)) return NULL;
    // 否则返回第一个在范围内的节点
    return x;
}

先快速判断范围是否与跳表有交集(zslIsInRange),然后从最高层开始定位第一个>=min的节点,最后检查是否<=max。

zslLastInRange对称地定位最后一个<=max的节点。

这些函数是ZRANGEBYSCOREZREVRANGEBYSCORE等范围命令的底层支撑。

九、复杂度

操作 函数 平均 最坏
创建跳表 zslCreate O(1) O(1)
插入节点 zslInsert O(log n) O(n)
删除节点 zslDelete O(log n) O(n)
更新分数 zslUpdateScore O(1)~O(log n) O(n)
查找节点 zslFind O(log n) O(n)
获取排名 zslGetRank O(log n) O(n)
范围查询起点 zslFirstInRange O(log n) O(n)
范围遍历 逐层 forward O(log n + m) O(n)
获取长度 zsl->length O(1) O(1)

注:m为范围内节点数,最坏情况O(n)发生在极端不平衡时(概率极低)。

十、为什么选择跳表而不是红黑树?

在实现有序集合(ZSet)时,需要一个既能按score排序、又支持高效查找和范围查询的数据结构。业界最经典的答案是红黑树 ------Java的TreeMap、C++的std::map都选择了它。但Redis的作者Antirez选择了跳表

这不是一个随意的决定。Antirez在Redis邮件列表和多个场合解释过这个选择,核心考量是ZSet的具体操作需求。

10.1 ZSet需要哪些操作?

先看Redis有序集合的核心命令及其底层操作:

命令 底层操作 复杂度要求
ZADD 插入/更新元素 O(logn)
ZREM 删除元素 O(logn)
ZSCORE 按成员查分数 O(1)
ZRANK 查排名 O(logn)
ZRANGE 按排名范围查询 O(logn+m)
ZRANGEBYSCORE 按分数范围查询 O(logn+m)
ZREVRANGE 反向范围查询 O(logn+m)
ZINCRBY 增减分数 O(logn)
ZCARD 获取元素数 O(1)
ZCOUNT 范围内计数 O(logn)

10.2 跳表vs红黑树

10.2.1 基本操作复杂度
操作 跳表 红黑树
查找 O(logn) O(logn)
插入 O(logn) O(logn)
删除 O(logn) O(logn)
范围查询 O(logn+m) O(logn+m)
排名查询 O(logn)* O(logn)*
反向遍历 O(m) O(m)

跳表通过span字段天然支持O(logn)排名;红黑树需要额外的子树大小字段(顺序统计树)才能实现O(logn)排名。基本操作复杂度两者相当,没有本质差异。

10.2.2 范围查询------跳表的优势项

跳表的范围查询

复制代码
找到起点(O(logn))→沿Level 0的forward逐个遍历(O(m))

跳表的Level 0本身就是一个有序链表,范围遍历只需沿forward指针走,缓存友好,指针跳转少。

红黑树的范围查询

复制代码
找到起点(O(logn))→找后继节点(每步O(logn))→重复m次

红黑树的中序遍历需要不断找后继,每次可能向上回溯再向下,指针跳转多,缓存不友好。

实测影响:范围查询是ZSet最高频的操作之一,跳表的线性遍历在m较大时明显快于红黑树的中序遍历。

10.2.3 反向遍历------跳表的优势项

跳表 :Level 0有backward指针,反向遍历就是沿backward走,O(m)。

红黑树 :需要找前驱节点(prev),每次可能涉及多次指针跳转,O(m*logn)最坏,O(m)平均但常数更大。

Redis的ZREVRANGEZREVRANGEBYSCORE等反向命令依赖此操作。

10.2.4 排名查询------跳表天然支持

跳表span字段记录每层跨越的节点数,查找时累加span即可得到排名,O(logn)。

红黑树 :原生不支持排名查询。要实现O(logn)排名,需要将红黑树扩展为顺序统计树(Order Statistic Tree),每个节点额外维护子树大小字段。这增加了:

  • 额外的内存开销
  • 插入/删除时维护子树大小的额外逻辑
  • 旋转时重新计算子树大小的复杂度
10.2.5 实现复杂度------跳表完胜
维度 跳表 红黑树
插入 找位置+随机层数+修改指针 找位置+插入+旋转重平衡
删除 找位置+修改指针 找位置+删除+旋转重平衡
实现复杂度 低(无旋转,逻辑线性) 高(旋转状态多,边界条件复杂)

红黑树的插入和删除需要旋转来维护平衡,旋转涉及大量指针操作和颜色调整:

复制代码
红黑树删除的旋转情况:
- 兄弟为红色
- 兄弟为黑色,两个侄子为黑色
- 兄弟为黑色,远侄子为黑色,近侄子为红色
- 兄弟为黑色,远侄子为红色

相比之下,跳表的插入只需随机层数+修改前驱后继指针,删除只需修改指针,逻辑极其直观。

10.2.6 并发友好性------跳表的优势

虽然Redis是单线程的,但这个对比仍然有参考价值:

跳表:插入和删除只影响局部节点,锁粒度可以很小(细粒度锁或CAS)。

红黑树:插入和删除可能触发旋转,旋转影响范围大,锁粒度难控制。

Java的ConcurrentSkipListMap选择跳表而非ConcurrentTreeMap就是这个原因。

10.2.7 内存占用------跳表略逊
数据结构 每节点额外指针/字段
跳表 平均1.33层×(forward+span)+backward≈3.66个字段
红黑树 left+right+parent+color=4个字段

跳表每个节点平均约4.66个字段(含span),红黑树每个节点4个字段。跳表略多,但差距不大(关于P=0.25的选择原因详见第二章)。

Antirez在Redis Google Group中回答过这个问题,核心观点:

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 be in a given level will make then less memory intensive than btrees.
  2. A sorted set is often target of many ZRANGE or ZREVRANGE operations. Traversing a skip list is like traversing a linked list, while with a tree we need to perform tree rotations or to use a threaded tree (adding two more pointers per node).
  3. The implementation is much simpler than a balanced tree.

翻译:

  1. 内存可控:通过调整概率参数,跳表的内存可以比B树更少
  2. 范围遍历高效:跳表的范围遍历像遍历链表一样简单;红黑树需要旋转或用线索树(额外两个指针)
  3. 实现简单:比平衡树简单得多

通过上面对跳表的分析,我们系统梳理了Redis跳表的完整实现:从节点结构的精巧设计(span字段支持 O(log n) 排名查询),到随机层数的概率机制(P=0.25 在性能与内存间取得平衡),再到插入、删除、更新等核心操作的算法细节。跳表以简洁的代码和线性的逻辑,实现了与红黑树相当的性能,同时在范围查询、反向遍历和实现复杂度上展现出显著优势。

跳表是Redis有序集合(ZSet)高效运作的基石。在实际应用中,ZSet通过字典+跳表的双重索引结构,同时满足了 O(1) 按成员查分数(字典)和 O(log n) 按分数排序(跳表)的需求。理解跳表的实现原理,是深入掌握ZSet整体架构的关键前提。

相关推荐
虹科网络安全5 小时前
艾体宝产品|深度解读 Redis 8.4 新增功能:原子化 Slot 迁移(下)
数据库·redis·bootstrap
快乐非自愿8 小时前
Redis--SDS字符串与集合的底层实现原理
数据库·redis·缓存
无小道10 小时前
Redis——特性
redis
子木HAPPY阳VIP11 小时前
信创UOS,Docker 完整操作部署(Dockerfile部署方式)&排错整合
linux·运维·redis·nginx·docker·容器·tomcat
手握风云-12 小时前
Redis:不只是缓存那么简单(四)
redis·缓存
冷小鱼14 小时前
Valkey 深度剖析:Redis 最佳平替的技术全景
数据库·redis·缓存·valkey
星筏15 小时前
深入理解分布式锁:ZooKeeper vs Redis
redis·分布式·zookeeper
Knight_AL15 小时前
从 0 到 1:PG WAL → Debezium → Kafka → Spring Boot → Redis
spring boot·redis·kafka
冷小鱼15 小时前
Redis 技术全景解析:从缓存基石到 AI 时代的数据引擎
数据库·redis·缓存