
redis跳表——zset的底层实现
博客主页:🏆看看是李XX还是李歘歘🏆🌺每天不定期分享一些包括但不限于计算机基础、算法、后端开发相关的知识点,以及职场小菜鸡的生活。🌺💗点关注不迷路,总有一些📖知识点📖是你想要的💗⛽️今天的内容是redis跳表——zset的底层实现⛽️💻💻💻redis的有序集合zset在增删改查的性质上类似于C++ stl的map和Java的TreeMap,提供了一组“键-值”对,并且“键”
博客主页:🏆看看是李XX还是李歘歘 🏆
🌺每天不定期分享一些包括但不限于计算机基础、算法、后端开发相关的知识点,以及职场小菜鸡的生活。🌺
💗点关注不迷路,总有一些📖知识点📖是你想要的💗
⛽️今天的内容是 redis跳表——zset的底层实现 ⛽️💻💻💻
redis的有序集合zset在增删改查的性质上类似于C++ stl的map和Java的TreeMap,提供了一组“键-值”对,并且“键”按照“值”的顺序排序。但是与C++ stl或Java的红黑树实现不同的是,redis中有序集合的实现采用了另一种数据结构——跳跃表。跳跃表是有序单链表的一种改进,其查询、插入、删除也是O(logN)的时间复杂度。
zset的两种实现方式
- ziplist(压缩链表):满足以下两个条件的时候
- 元素数量少于128的时候
- 每个元素的长度小于64字节
- skiplist(跳跃链表):不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist
- map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的分数
- skiplist按从小到大的顺序存储分数
- skiplist每个元素的值都是[score,value]对
ziplist原理
每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。压缩列表中的元素按照分数从小到大依次紧挨着排列,有效减少了内存空间的使用。
skiplist原理
参考:Skip Lists: A Probabilistic Alternative to Balanced Trees
上图用a,b,c,d,e五种有序链表说明了跳跃表的motivation.
- [a]单链表:查询时间复杂度O(n)
- [b]level-2单链表:每隔一个节点为一个level-2节点,每个level-2节点有2个后继指针,分别指向单链表中的下一个节点和下一个level-2节点。查询时间复杂度为O(n/2)
- [c]level-3单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,查询时间复杂度O(n/4)
- [d]指数式单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,每隔8个节点为一个level-4节点(每2^i个节点的level为i+1),查询时间复杂度为O(log2N)
- [e]跳跃表:各个level的节点个数同指数式单链表,但出现的位置随机,查询复杂度是O(logN)
因为有序链表的插入和删除复杂度等于查询复杂度。文章证明了跳跃表查询复杂度的期望是O(logN).
redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。
redis跳跃表的源码
跳跃表节点,跳跃表和zset结构体的定义在server.h
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
score:分值,用于排序
backward:是第一层的前一个数据,即span=1
level[]:每一个层所代表的节点node
forward:该层级的下一个节点
span:到达该层级的下一个节点,实际跨越了多少个节点,也是方便用于zrange等排行榜查询的用处
skiplist过程:
1. 查找过程 时间复杂度O(logN)
对于zrangebyscore命令:score作为查找的对象,在跳表中跳跃查询,就和上面skiplist的查询一样
2. 插入过程 时间复杂度O(logN)
zadd [zset name] [score] [value]:
- 在map中查找value是否已存在,如果存在现需要在skiplist中找到对应的元素删除,再在skiplist做插入
- 插入过程也是用score来作为查询位置的依据,和skiplist插入元素方法一样。并需要更新value->score的map
如果score一样怎么办?根据value再排序,按照顺序插入
3. 删除过程
zrem [zset name] [value]:从map中找到value所对应的score,然后再在跳表中搜索这个score,value对应的节点,并删除
4. 排名是怎么算出来的
zrank [zset name] [value]的实现依赖与一些附加在跳表上的属性:
- 跳表的每个元素的Next指针都记录了这个指针能够跨越多少元素,redis在插入和删除元素的时候,都会更新这个值
- 然后在搜索的过程中按经过的路径将路径中的span值相加得到rank
总结:
(1)跳表是可以实现二分查找的有序链表;
(2)每个元素插入时随机生成它的level;
(3)最低层包含所有的元素;
(4)如果一个元素出现在level(x),那么它肯定出现在x以下的level中;
(5)每个索引节点包含两个指针,一个向下,一个向右;
(6)跳表查询、插入、删除的时间复杂度为O(log n),与*衡二叉树接*;
redis有序集合zset的底层实现——跳跃表skiplist_da_kao_la的博客-CSDN博客_zset底层实现
跳跃表是有序集合的底层实现之一。
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
每个跳跃表节点的层高都是1至32之间的随机数。
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
为什么采用跳表,而不使用哈希表或平衡树实现呢?
哈希表不是有序的
平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
更深层次:redis有序集合zset的底层实现——跳跃表skiplist_wrr-cat的博客-CSDN博客_zset底层设计
更多推荐










所有评论(0)