介绍
Zset,即有序集合(Sorted Set),是 Redis 提供的一种复杂数据类型。Zset 是 set 的升级版,它在 set 的基础上增加了一个权重参数 double类型的分数值(即score),使得集合中的元素能够按 score 进行有序排列。
zset的listpack结构
当满足以下两个条件时, zset采用listpack结构存储数据
- 集合中元素数量小于等于128个
- 集合中每个元素小于等于64k
存储结构如下
两个条目为一组, 分别存储实际值和score, 同时listpack也支持查询步长+1的跨越条目查询, listpack本身是无序的,但默认最多只会存储64条所以每次获取时才扫描顺序
zset的skiplist+dict结构
当zset中的元素不满足以上两个条件时, 会采用skiplist + dict存储数据
skiplist 是基于随机算法的一种有序链表,其查询效率与红黑树的查询效率差不多,都是 O(logn),但是 skiplist 的结构远比复杂的红黑树简单很多。下面我们就深入介绍一下 skiplist 的结构。
Redis 定义了一个 zskiplist 结构体来抽象 skiplist 这一数据结构,其具体定义如下:
// 跳表本体
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 指向skiplist结构的首节点和尾结点
unsigned long length; // 当前zskiplist level0(底层)的节点个数,也是其中存储元素的个数
int level; // 当前zskiplist的层级数
} zskiplist;
// 跳表节点
typedef struct zskiplistNode {
sds ele; // 元素值(成员)
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度(指向目标节点之间的距离)
} level[];
} zskiplistNode;
跳表的查找、插入和删除操作的时间复杂度都是 O(logN),其中 N 是跳跃表中的元素数量。这使得跳表在处理大量数据时具有很高的性能。跳表在链表的基础上增加了多级索引,通过多级索引位置的专跳,实现了快速查找元素
我们常见的有序链表结构如下图所示,每个节点里面有两个关键部分,一个是存放的数据,还有一个是指向下一个节点的 next 指针。
假设要在这个链表里面查询 48 这个节点,我们需要从 header 节点开始向后遍历,每遍历到一个节点,都要比较节点存储的值与 48 是否一致。最差的情况就是遍历完整个链表,时间复杂度也就是 O(N)。
skiplist 在普通有序链表的基础之上,添加多层有序链表,如下图所示:
使用 skiplist 查找数据的时候,会先从最高层,也就是上图中的 level 2,开始向后遍历,这里发现 level 2 层小于 48 的最大节点是 18 节点,那么向下一层来到 level 1,继续从 18 节点向后遍历。在 level 1 层中发现小于 48 的最大节点是 35,那么再向下一层来到 level 0,继续从 35 节点向后迭代,最终找到 48 这个目标节点。
整个查找路径如下图红色箭头所示:
通过这个示例,我们可以体会到使用 skiplist 在结构上的特点:维护多层有序链表的索引,越往上层索引越稀疏,比如,level 2 就比 level 1 稀疏。在查找过程中,这些稀疏索引可以帮助我们跳过一些不必要的匹配操作。在利用 skiplist 这种稀疏索引进行查找的时候,我们需要使用到 cur 和 next 两个指针,当 next 节点的值比目标值大或是迭代到 NULL 时,则下降一层继续从 cur 开始向后遍历,直至找到目标节点或者返回 NULL。
skiplist+dict结构图大致如下(借鉴图)
使用dict存储value, 使用skiplist存储socre
Redis跳表与MySQLB+树对比
特性 | B+树 | 跳表 |
---|---|---|
数据结构 | 多叉平衡树(每个节点可以有多个子节点),非叶子节点存索引,叶子存数据 | 多层链表结构,底层存数据,上层为索引,每个节点只有一个下一节点 |
查询复杂度 | O(logN) | O(logN) |
插入复杂度 | O(logN) | O(logN) |
删除复杂度 | O(logN) | O(logN) |
区间查询 | 高效,通过叶子节点链表实现范围查询 | 高效,逐层下降并线性遍历范围内数据 |
空间开销 | 每个节点存多个元素,占用空间少 | 多级索引链表,占用更多指针空间 |
磁盘友好性 | 磁盘 IO 友好,适合数据库和存储系统; 以页(通常4k)为单位读写 | 内存结构,不适合磁盘存储 |
稳定性 | 高效稳定,性能接近二分查找 | 稳定性略逊于 B+树,随机性较强 |
实现复杂度 | 实现复杂,涉及节点分裂和合并 | 实现简单,易于维护 |
缓存性能 | 数据顺序存储在叶子节点,缓存命中率高 | 链表遍历,不利于缓存命中 |
使用场景 | 数据库存储、磁盘存储 | Redis Zset、内存排序与排名 |
结论
- 数据库索引:选 B+树(磁盘 IO 更少)。
- 内存有序集合:选跳表(易实现、效率高)。
- 查询效率:在内存中,跳表略优;在磁盘上,B+树更优
个人公众号: 行云代码