redis zset原理

zset即有序集合sorted set,用分数进行成员的从小到大的排序。

2种数据结构

ziplist

条件:元素个数小于128,而且所有元素小于64字节。

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

image

基于双向链表保存压缩列表节点,保存member和score即元素。
查找复杂度是O(N)。

skiplist

条件:元素个数>=128,或者存在元素>=64字节。

这个结构体中包含一个字典和一个跳跃表。跳跃表按score 从小到大保存所有集合元素,查找时间复杂度为平均O(logN),最坏O(N)。字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score 值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。

skiplist

跳表具有以下几个特点:
1. 由许多层结构组成。
2. 每一层都是一个有序的链表。
3. 最底层 (Level 1) 的链表包含所有元素。
4. 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
5. 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

跳表的查找会从顶层链表的头部元素开始,然后遍历该链表,直到找到元素大于或等于目标元素的节点,如果当前元素正好等于目标,那么就直接返回它。如果当前元素小于目标元素,那么就垂直下降到下一层继续搜索,如果当前元素大于目标或到达链表尾部,则移动到前一个节点的位置,然后垂直下降到下一层。正因为 Skiplist 的搜索过程会不断地从一层跳跃到下一层的,所以被称为跳跃表。

image

skiplist2

查找时间复杂度为平均O(logN),最差O(N)。在大部分情况下效率可与平衡树相媲美,但实现比平衡树简单的多,跳表是一种典型的以空间换时间的数据结构。

跳表在插入操作时,元素的插入层数完全是随机指定的。
该决定插入层数的随机函数对跳表的查找性能有着很大影响
具体步骤
1. 指定一个节点最大的层数 MaxLevel,指定一个概率 p, 层数 lvl 默认为 1 。
2. 生成一个 0~1 的随机数 r,若 r < p,且 lvl < MaxLevel ,则执行 lvl ++。
3. 重复第 2 步,直至生成的 r > p 为止,此时的 lvl 就是要插入的层数。

在Redis的skiplist实现中,p=1/4,MaxLevel=32。

Redis中的 Skiplist 与经典 Skiplist 相比,有如下不同:
1. 分数(score)允许重复,即 Skiplist 的 key 允许重复,经典 Skiplist 中是不允许的。
2. 在比较时,不仅比较分数(相当于 Skiplist 的 key),还比较数据本身。在 Redis 的 Skiplist 实现中,数据本身的内容唯一标识这份数据,而不是由 key 来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。
3. 第 1 层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。

Skiplist与平衡树、哈希表的比较
1. Skiplist 和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个 key 的查找,不适宜做范围查找。
2. 在做范围查找的时候,平衡树比 Skiplist 操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
3. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而 Skiplist 的插入和删除只需要修改相邻节点的指针,操作简单又快速。
4. 从内存占用上来说,Skiplist 比平衡树更灵活一些。一般来说,平衡树每个节点包含 2 个指针(分别指向左右子树),而 Skiplist 每个节点包含的指针数目平均为1/(1−p),具体取决于参数 p的大小。如果像 Redis 里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
5. 查找单个 key,Skiplist 和平衡树的时间复杂度都为 O(logN);而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近 O(1),性能更高一些。从算法实现难度上来比较,Skiplist 比平衡树要简单得多。

Redis Zset 采用跳表而不是平衡树的原因
1. 也不是非常耗费内存,实际上取决于生成层数函数里的概率 p,取决得当的话其实和平衡树差不多。
2. 因为有序集合经常会进行 ZRANGE 或 ZREVRANGE 这样的范围查找操作,跳表里面的双向链表可以十分方便地进行这类操作。
3. 实现简单,ZRANK 操作还能达到 O(logN)的时间复杂度。

参考资料

https://marticles.github.io/2019/03/19/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Redis-Zset%E5%8E%9F%E7%90%86/
https://cloud.tencent.com/developer/article/2183817
https://blog.csdn.net/qq_40707462/article/details/122973441

posted on 2026-04-15 08:18  王景迁  阅读(7)  评论(0)    收藏  举报

导航