Redis——zset底层结构

本文深入解析Redis中ZSet数据结构的实现原理,包括其使用的散列表和跳跃列表,以及如何通过跳跃列表实现高效的查找和插入操作。文章还讨论了在所有元素score值相同时Redis如何确保正确排序。

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

redis中 zset 底层采用散列表+跳跃列表(skiplist)来存储数据。

散列表不用多说,set 底层采用散列表来存储,value都为null,通过散列表key的唯一性保证set中元素的不重复。

跳跃列表的结构:

上图就是跳跃列表的示意图,图中只画了四层,Redis 的跳跃表共有 64 层,意味着最 多可以容纳 2^64 次方个元素。

每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kv header 也是这个结构,只不过 value 字段是 null 值,score为Double.MIN_VALUE,用来垫底的。

底层的 kv 之间使用指针串起来形成了双向链表结构,它们是有序排列的,从小到大。

struct zslnode {
    string value;
    double score;
    zslnode*[] forwards; // 多层连接指针
    zslnode* backward; // 回溯指针
}

不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。

随机层数

对于新插入的节点,需要调用一个随机算法分配合理的层数。

直观上期望的目标是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层 2^-63,因为这里每一层的晋升概率是 50%。

不过 Redis 标准源码中的晋升概率只有 25%,也就是代码中的 ZSKIPLIST_P 的值。所以官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。

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

因为层数一般不高,所以遍历的时候从顶层开始往下遍历会非常浪费。跳跃列表会记录一下当前的最高层数 maxLevel,遍历时从这个 maxLevel 开始遍历性能就会提高很多。

查找过程

通过逐层查找的方式来查找数据,时间复杂度为 O(logn)

如图所示,我们要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个节点 (最后一个比「我」小的元素)。

然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素)。

然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比「我」小的元素)。

插入过程

查找过程中已经找到 最底层的最后一个比“我”小的元素,插入时,只要在这个元素后插入元素即可。

插入时,需修改前后元素的指针。如果新节点的高度大于当前的最大高度,需更新当前的最大高度。

如果所有元素的score值都相同呢?

在一个极端的情况下,zset 中所有的 score 值都是一样的,zset 的查找性能会退化为O(n) 么?Redis 作者自然考虑到了这一点,所以 zset 的排序元素不只看 score 值,如果score 值相同还需要再比较 value 值 (字符串比较)。

 

 

### Redis ZSet 底层实现 #### 1. 跳表 (Skip List) Redis 中的有序集合(ZSet)主要依赖于跳表这一数据结构来维持元素之间的顺序。跳表是一种可以在对数时间内完成插入、删除和查找操作的数据结构,其设计灵感来源于链表与二分查找的思想融合。通过多级索引来加速查询过程,在最坏情况下也能保持 O(log N) 的时间复杂度[^1]。 为了提高效率并减少内存占用,Redis 并未完全遵循传统意义上的跳表定义;而是做了一些优化调整: - **双向指针**:每一层不仅有向前指向更高概率级别的链接,还保留了向后的指针以便快速回溯。 - **随机化级别生成算法**:用于决定新加入节点应该位于哪几层上,通常会控制较高层数的比例较小以平衡空间开销与访问速度间的权衡。 ```c typedef struct zskiplistNode { sds ele; /* 元素 */ double score; /* 分数值 */ struct zskiplistLevel { /* 各层信息 */ struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode; ``` 这段 C 代码展示了 `zskiplistNode` 结构体的设计,其中包含了实际存储的对象 (`ele`) 和它的评分 (`score`),以及一个动态数组用来表示不同层次上的连接关系[^4]。 #### 2. 压缩列表 (ZipList) 对于小型且简单的场景下,即当满足以下两个条件之一时,Redis 将采用更紧凑高效的压缩列表形式来代替跳表作为内部容器: - 当前有序集合计数少于配置参数指定的最大阈值,默认为 128; - 所有成员字符串长度不超过给定最大尺寸限制,默认是 64 字节。 在这种模式下,整个 ZSet 实际被编码成单一连续区块内的多个字段序列,从而节省了大量的间接寻址成本,并简化了许多基础性的维护工作。不过一旦超出上述约束,则立即转换至标准版 skip list 架构运行[^5]。 #### 3. Hash 表辅助映射 无论是哪种具体形态,都会额外配备一张哈希表用作从对象到对应得分之间的一一映射机制。这样做的好处是可以极大地加快针对特定项执行增删改查类指令的速度——因为无需再遍历整条链条就能准确定位目标位置所在。 综上所述,Redis 设计者们精心挑选并改进过的这两种核心组件共同构成了如今我们所熟知的强大而又灵活易用的 ZSet 功能模块。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值