文章目录
一、ZSet有序集合类型
二、ZSet底层结构详解
三、Redis跳表与MySQL B+树
四、Hash、B+树、跳表的比较
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
ZSet(有序集合)即SortedSet,是一个自动根据元素score排序的有序集合。它是一个可排序的set集合,在 Set 的基础上增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。在 Redis 中,有序集合的最大成员数是 2^32 - 1。ZSet具备下列特性:
在 Zset 中,集合元素的添加、删除和查找的时间复杂度都是 O(logn),这得益于 Redis 使用跳表SkipList来实现 Zset。
因为ZSet的可排序特性,经常被用来实现排行榜这样的功能。
以上只是 ZSet 的一些常见应用场景,实际上Zset 的应用非常广泛,只要是需要排序和排名功能的场景,都可以考虑使用 ZSet。
ZSet与Java中的TreeSet有些类似,但底层数据结构却差别很大。ZSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序。底层实现有两种方式:当元素较少或总体元素占用空间较少时,使用压缩列表ZipList来实现;当不符合使用压缩列表的条件时,使用跳表SkipList+ 字典hashtable来实现。注意,集合成员是唯一的,但是评分可以重复。
补充:ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:
注意事项:
ZSet的常见命令有:
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
127.0.0.1:6379> zadd zset1 1 first 2 second 3 third 4 four #往zset添中加一个或多个元素
(integer) 4
127.0.0.1:6379> zrange zset1 0 -1 #返回有序集合中、下标在<start> <end>之间的元素(有 WITHSCORES 会显示评分)。0 -1 表示所有元素
1) "first"
2) "second"
3) "third"
4) "four"
127.0.0.1:6379> zrevrange zset1 0 -1
1) "four"
2) "third"
3) "second"
4) "first"
127.0.0.1:6379> zscore zset1 third #获取zset中的third元素的score值
"3"
127.0.0.1:6379> zrank zset1 third #返回third在集合中的排名,从0开始
(integer) 2
127.0.0.1:6379> zrevrank zset1 third
(integer) 1
127.0.0.1:6379> zrangebyscore zset1 -inf +inf #给zset集合中的元素从小到大排序,-inf:负无穷,+inf:正无穷。返回score值介于-inf到+inf之间(含两端)的成员(score从小到大)
1) "first"
2) "second"
3) "third"
4) "four"
127.0.0.1:6379> zrangebyscore zset1 -inf +inf withscores #从小到大排序并输出键值
1) "first"
2) "1"
3) "second"
4) "2"
5) "third"
6) "3"
7) "four"
8) "4"
127.0.0.1:6379> zrangebyscore zset1 -inf 1 withscores #指定负无穷到1的范围
1) "first"
2) "1"
127.0.0.1:6379> zrem zset1 four #移除zset集合中指定的元素
(integer) 1
127.0.0.1:6379> zcard zset1 #查看zset集合中元素个数
(integer) 3
127.0.0.1:6379> zrangebyscore zset1 1 2 withscores #根据score值从小到大排列
1) "first"
2) "1"
3) "second"
4) "2"
127.0.0.1:6379> zrevrangebyscore zset1 2 1 withscores #根据score值从大到小排列
1) "second"
2) "2"
3) "first"
4) "1"
127.0.0.1:6379> zcount zset1 1 2 #统计score值在1到2之间的元素个数
(integer) 2
有序集合Zset底层实现会根据实际情况选择使用压缩列表(ziplist)或者跳跃表(skiplist):当元素较少或总体元素占用空间较少时,使用压缩列表ZipList来实现;当不符合使用压缩列表的条件时,使用跳表SkipList+ 字典hashtable来实现。
具体细节可参考本文1.3小节
ZipList是一种特殊的“双端链表”(并非链表),由一系列特殊编码的连续内存块组成,像内存连续的数组。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。
压缩列表 底层数据结构:本质是一个数组,增加了列表长度、尾部偏移量、列表元素个数、以及列表结束标识,有利于快速寻找列表的首尾节点;但对于其他正常的元素,如元素2、元素3,只能一个个遍历,效率仍没有很高效。
属性 | 类型 | 长度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 一个 4 字节的整数,表示整个压缩列表占用的字节数量,包括 |
zltail | uint32_t | 4字节 | 一个 4 字节的整数,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址 |
zllen | uint16_t | 2字节 | 一个 2 字节的整数,表示压缩列表中的节点数量。最大值为UINT16_MAX(65534),如果超过这个数,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算出 |
entry | 列表节点 | 不定 | 压缩列表中的元素,每个元素都由一个或多个字节组成,节点的长度由节点保存的内容决定。每个元素的第一个字节(又称为"entry header")用于表示这个元素的长度以及编码方式 |
zlend | uint8_t | 1字节 | 一个字节,特殊值0xFF(十进制255),表示压缩列表的结束 |
注意:
学习一个新知识,从三方面分析:WHAT、WHY、HOW
**SkipList(跳表)**首先是链表,在有序链表的基础上,增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素。但与传统链表相比有几点差异:
在 Redis 源码中,跳跃表的结构定义如下:
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
跳表查找、插入和删除操作的时间复杂度都是 O(logN)。
SkipList的特点:
对于一个单链表来说,即使链表中的数据是有序的,如果我们想要查找某个数据,也必须从头到尾的遍历链表,很显然这种查找效率是十分低效的,时间复杂度为O(n)。
普通链表想查找元素27,只能从链表头部一个个往后遍历,需要遍历6次 才能找到元素27
跳表怎么做的(how):建立多级索引
如建立一级索引
如果觉得慢,可以建立二级索引
当数据量特别大的时候,跳表的时间复杂度为 O(logN)。其本身利用的思想,有点类似于二分法。
因为普通链表查找一个元素 时间复杂度O(n);而跳表查找的时间复杂度为O(logn),查找速度更快。不仅如此,删除、插入等操作的时间复杂度也是O(logn)
SkipList作为ZSet的存储结构,整体存储结构如下图,核心点主要是包括一个dict对象和一个skiplist对象。dict保存key、value,key为元素,value为分值;skiplist保存的有序的元素列表,每个元素包括元素和分值。
上图中 zskiplist 结构包含以下属性:
位于 zskiplist 结构右侧是四个 zskiplistNode 结构,该结构包含以下属性:
什么时候采用压缩列表、什么时候采用跳表呢
上述 1且2的时候,采用压缩列表;否则采用跳表
Redis跳表:
B+Tree:
MySQL B+树:
相比于标准的B+树,InnoDB使用的B+树有如下特点:
区别:
MySQL 的 B+ 树、Redis 的跳表都是用于存储有序数据的数据结构,但它们有一些关键的区别,使得它们在不同的场景和用途中各有优势。
因此,B+ 树和跳表不能简单地相互替换。在需要大量进行磁盘 I/O 操作和范围查询的场景(如数据库索引)中,B+ 树可能是更好的选择;而在主要进行内存操作,且需要频繁进行插入和删除操作的场景(如 Redis)中,跳表可能更有优势。
MySQL是持久化数据库、即存储到磁盘上,因此查询时要求更少磁盘 IO,且 Mysql 是读多写少的场景较多,显然 B+ 树更加适合Mysql。
Redis是直接操作内存的、并不需要磁盘io;而MySQL需要去读取磁盘io,所以MySQL使用b+树的方式去减少磁盘io。B+树原理是 叶子节点存储数据、非叶子节点存储索引,每次读取磁盘页时就会读取一整个节点,每个叶子节点还要指向前后节点的指针,其目的是最大限度地降低磁盘io
数据在内存中读取 耗费时间是磁盘IO读取的百万分之一,而Redis是内存中读取数据、不涉及IO,因此使用了跳表,跳表模型是更快更简单的方式
1)ZSet为什么不用B+树,而用跳表
总的来说,Redis选择跳表作为有序集合数据结构的底层实现,是基于跳表本身的优点:时间复杂度优势、简单高效、空间利用率高和并发性能好。这使得Redis在处理有序集合的操作时能够获得较好的性能和并发能力。Redis是内存数据库、不存在IO的瓶颈,而B+树纯粹是为了MySQL这种IO数据库准备的。B+树的每个节点的数量都是一个MySQL分区页的大小。
2)ZSet为什么不用红黑树、二叉树
红黑树、二叉树查找一个元素的时间复杂度也是O(logn)
数据结构 | 实现原理 | key查询方式 | 查找效率 | 存储大小 | 插入、删除效率 |
---|---|---|---|---|---|
Hash | 哈希表 | 支持单key | 接近O(1) | 小,除了数据没有额外的存储 | O(1) |
B+树 | 平衡二叉树扩展而来 | 单key,范围,分页 | O(logn) | 除了数据,还多了左右指针,以及叶子节点指针 | O(logn),需要调整树的结构,算法比较复杂 |
跳表 | 有序链表扩展而来 | 单key,分页 | O(logn) | 除了数据,还多了指针,但是每个节点的指针小于<2,所以比B+树占用空间小 | O(logn),只用处理链表,算法比较简单 |
参考
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。