Redis底层数据结构之 zset
前言
zset
是Redis
提供的一个非常特别的数据结构,常用作排行榜等功能。zset
在Redis
中两种不同的实现,分别是zipList
和skipList
。zipList
前面我们已经介绍过了,这里就不再介绍了。具体使用哪种结构进行存储,规则如下:
zipList
:需要满足以下两个条件[score,value]
键值对数量少于128个;- 每个元素的长度小于64字节;
skipList
:不满足以上两个条件时使用跳表、组合了hash
和skipList
hash
用来存储value
到score
的映射,这样就可以在O(1)
时间内找到value
对应的分数;skipList
按照从小到大的顺序存储分数skipList
每个元素的值都是[socre,value]
对
使用zipList
的示意图如下所示:
使用跳表时的示意图:
跳表skipList
跳表可以保证增、删、查等操作时的时间复杂度为O(logN)
,这个性能可以与AVL
平衡二叉树相媲美,但实现方式上却更加简单,唯一美中不足的就是跳表占用的空间比较大,其实就是一种空间换时间的思想。跳表的结构如下所示:
Redis
中跳表一个节点最高可以达到64层,一个跳表中最多可以存储2^64个元素。跳表中,每个节点都是一个skiplistNode
,每个跳表的节点也都会维护着一个score
值,这个值在跳表中是按照从小到大的顺序排列好的。
跳表的结构定义如下所示:
typedf struct zskiplist{
//头节点
struct zskiplistNode *header;
//尾节点
struct zskiplistNode *tail;
// 跳表中元素个数
unsigned long length;
//目前表内节点的最大层数
int level;
}zskiplist;
各属性含义如下:
header:指向跳表的头节点,通过这个指针可以直接找到表头,时间复杂度为O(1)
tail:指向跳表的尾节点,通过这个指针可以直接找到表尾,时间复杂度为o(1)
length:记录跳表的长度,即不包括头节点,整个跳表中有多少个元素
level:记录当前跳表内,所有节点中层数最大的level
zskiplist
的示意图如下所示:
zskiplistNode
的结构定义如下:
typedf struct zskiplistNode{
// 具体的数据
sds ele;
// 分数
double score;
//后退指针
struct zskiplistNode *backward;
//层级数组 最大32
struct zskiplistLevel{
//前进指针forward
struct zskiplistNode *forward;
//跨度span
unsigned int span;
}level[];
}zskiplistNode;
各属性含义如下:
-
ele:真正的数据,每个节点的数据都是唯一的,但节点的分数
score
可以是一样的。两个相同分数score
的节点是按照元素的字典序进行排列的 -
score:各个节点中的数字是节点所保存的分数
score
,在跳表中,节点按照各自所保存的分数从小到大排列; -
backward:用于从表尾向表头遍历,每个节点只有一个后退指针,即每次只能后退一步;
-
zskiplistLevel:层级数组,这个数组中的每个节点都有两个属性,
forward
指向同层的下一个节点,span
跨度用来计算当前节点在跳表中的一个排名,这就为zset
提供了一个查看排名的方法。数组中的每个节点中用1、2、3等字样标记节点的各个层,分别L1
代表第一层,L2
代表第二层,L3
代表第三层,以此类推;
skiplistNode
的示意图如下所示:
增删改查
以下图为例,讲解一下skiplist
的增删改查过程。
增
比如要插入的值为6
- 从
head
节点开始,先是在head
开始降层来查找到最后一个比6
小的节点 - 等到查到最后一个比
6
小的节点的时候(假设为5
) - 然后需要引入一个随机层数算法来为这个节点随机地建立层数
- 把这个节点插入进去以后,同时更新一遍最高的层数即可
改
先是判断这个value
是否存在,如果不存在就是新增的场景,反之,就是更新的场景。
需要注意的是,在skipList
在更新时,是先找到了value
,把这value
个删除掉,然后新增。这样的话就会做两次的搜索,在性能上来讲就比较慢了,因此在 Redis 5.0 版本中,Redis 的作者Salvatore Sanfilippo大神就优化了这个更新的过程。目前新的更新过程是,先判断这个 value是否存在,如果存在的话就直接更新,然后再调整整个跳跃表的score
排序,这样就不需要进行两次搜索,提高了性能。
查
假设现在要查找7
这个节点,步骤如下:
- 从
head
开始遍历,指针指向4
这个节点,由于4<7
,且同层的下一个指针指向NULL
,所以去到下降一层; - 跳到
6
节点所在的层,同理,6<7
,且同层的下一个指针指向NULL
,再下降一层; - 此时到了第一层,第一层是一个双向链表,由于
6<7
,所以开始向后遍历,查找到7
就返回值,不然就返回NULL
;
删
删除的过程前期与查找相似,先定位到元素所在的位置,再进行删除,最后更新一下指针、更新一下最高的层数。
随机层数算法
skipList
的新增数据时会使用到一个随机层数算法,这个算法的主要作用是随机生成一个数,过程也很简单,源码如下:
# define ZSKIPLIST_MAXLEVEL 32
# define ZSKIPLIST_P 0.25
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL)
? level : ZSKIPLIST_MAXLEVEL;
}