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])

updatei的新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整体架构的关键前提。

相关推荐
爬也要爬着前进12 小时前
redis主从搭建
数据库·redis·缓存
用户0953675158315 小时前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌17 小时前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆18 小时前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰19 小时前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_7275356219 小时前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf
William Dawson20 小时前
【华为云 MRS Redis 安全集群 Kerberos 认证接入实战(Spring Boot \+ Jedis)】
redis·安全·华为云
zhouhui00121 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
java·spring boot·redis·ai编程
吃饱了得干活21 小时前
扫码登录如何实现?从原理到实战
java·spring boot·redis
熊猫钓鱼>_>2 天前
Redis 突发缓存穿透:一次完整的定位复盘
数据库·人工智能·redis·缓存·ai·agent·智能