Redis因其高效的性能和丰富的类型而被广泛使用。在Redis提供的数据类型中, Zset(有序集合)类型是最复杂也是最强大的一种。
- Zset不仅可以存储键值对, 还可以为每一个元素分配一个分数, 然后根据这个分数进行排序。这使得Zset非常适用于排行榜、时间线等应用场景。
一、Zset数据类型
- Zset是set的升级, 它在set的基础上增加了一个权重参数socre, 使得集合中的元素可以按照socre进行排序。
- Zset中, 集合元素的添加、删除和查找的时间复杂度是O(1)。这主要是因为Redis使用了skiplist(跳跃表)来实现Zset。
- Zset主要特性:
- 唯一性, 和set类型一样, Zset中的元素也是唯一的;
- 排序, Zset中的元素有序的, 它们的顺序是按照score的值从小到大排列。如果多个元素有相同的score, 那么它们会按照字典顺序。
- 自动更新排序, 当修改Zset中元素的score值时, 元素的位置会按新的score进行调整。
二、Zset的应用场景:
- 排行榜: 如使用用户ID作为元素, 用户的分数作为score值, 然后使用Zset来存储和排序所有用户的分数。可以很简单的获取到分数最高的用户, 或者获取到任何用户的分数。
- 时间线: 如把发布的消息作为元素, 发布消息时间作为score值, 然后使用Zset来存储和排序所有的消息。可以很简单的获取到最新消息, 或者获取到任何时间段内的消息。
- 延时队列: 可以把要延时处理的任务作为元素, 任务执行的时间作为score值, 然后使用Zset来存储和排序所有的任务。可以定期扫描Zset, 处理已经到执行时间的任务。
- 带权重的队列: 可以把任务作为元素, 任务的权重作为score值, 然后使用Zset来存储和排序所有的任务。可以很容易的获取到优先级最高的任务, 或者按照优先级顺序执行任务。
三、Zset的数据结构
- Zset底层数据结构
- Redis的Zset底层会根据实际情况选择使用ziplist(压缩列表)或者skiplist(跳跃列表)来实现。Redis会根据实际情况在这两种数据结构之间切换, 以在内存和性能之间找到一个平衡。
- 使用ziplist: 当Zset中存储的元素小于zset-max-ziplist-entries, 并且所有元素的最大长度小于zset-max-ziplist-value时候, Redis会选择使用ziplist作为底层实现。压缩列表所占的内存较少, 但是在需要修改元素的时候, 可能需要对整个压缩列表进行重写, 性能较低。
- 使用skiplist:当Zset中存储的元素大于zset-max-ziplist-entries, 并且任一元素的长度大于zset-max-ziplist-value时候, Redis会将Zset底层结构从ziplist替换到skiplist。跳跃表的查找和修改元素的性能高, 但是占用内存较高。
- 可以根据自己应用系统的特性, 选择更倾向于性能或者节省内存的方式, 以灵活配置zset-max-ziplist-entries和zset-max-ziplist-value。
- 压缩列表(ziplist):
- ziplist是一种为了节省内存而设计的特殊编码结构。它将所有的元素和分数紧凑的存储在一起。
- 优点是占用内存少。
- 缺点是在需要修改数据时候, 可能需要对整个ziplist进行重写, 性能较低。
- 当zset存储的元素数量较少并且元素字符串长度较短时, Redis会选择使用ziplist来实现Zset。
+---------+---------+--------+---------+---------+---------+--------+
| zlbytes | zltail | zllen | entry_1 | entry_2 | ... | zlend |
+---------+---------+--------+---------+---------+---------+--------+
| 4 | 4 | 2 | entry_1 | entry_2 | ... | 1(255) |
+---------+---------+--------+---------+---------+---------+--------+
//说明:
//zlbytes: ziplist总长度, 包括zlbytes;
//zltail: 压缩列表中最后一个元素的位置偏移量;
//zllen: 压缩列表中元素数量;
//entry: 你懂的;元素和score交替存储, 即entry_1为score, entry_2为元素;
//zend: 结束标识;
- 跳跃列表(skiplist):
-
跳跃表是一种可以进行快速查找的有序数据结构,它通过维护多级索引来实现快速查找。
-
这种方式的优点是查找和修改数据的性能较高,但是占用的内存也较多。
-
当 Zset 存储的元素数量较多,或者元素的字符串长度较长时,Redis 会选择使用跳跃表作为底层实现。
-
跳跃列表的查找、插入和删除的时间复杂度都是O(logN), 在处理大量数据的时候有很高的性能。
-
跳跃列表在链表的基础上增加了多级索引, 通过多级索引的位置转跳, 实现了元素的快速查找。
-
Redis中skiplist的定义如下:
-
// 跳跃表 节点定义;
typedef struct zskiplistNode {
robj *obj; //元素对象;
double score; //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;
+------+ +------+ +-------+ +-------+ +------+
| 5 | ---> | 6 | ---> | 8 | ---> | 13 | ---> | 22 | --->
+------+ +------+ +-------+ +-------+ +------+
+------+ +------+ +-------+
| 27 | ---> | 35 | ---> | 100 |
+------+ +------+ +-------+
1. 对上述链表进行查找时间复杂度为O(N), 如查找100为8;
- skiplist一级索引:
查找100, 遍历6次;
- skiplist二级索引:
查找100, 遍历6次;
-
插入元素18
- 原数据
- 插入变动
四、Zset与B+树对比
- Mysql的B+树都是用于存储有序数据的存储结构, 但他们存在一些关键区别, 以至它们使用场景有所区别。
- 结构差异: B+树是一种多路搜索树, 每个节点可以有多个字节点。而跳跃列表是一种基于链表的数据结构, 每个节点只有一个下一个节点, 但可以有多个快速通道指向后面的节点。
- 空间利用率: B+ 树的磁盘读写操作是以页(通常是 4KB)为单位, 每个节点存储多个键值对, 可以更好地利用磁盘空间, 减少 I/O 操作。而跳表的空间利用率相对较低。
- 插入和删除操作: 跳表的插入和删除操作相对简单, 时间复杂度为 O(logN), 并且不需要像 B+ 树那样进行复杂的节点分裂和合并操作。
- 范围查询: B+ 树的所有叶子节点形成了一个有序链表, 因此非常适合进行范围查询。而跳表虽然也可以进行范围查询, 但效率相对较低。
- 在需要大量进行磁盘 I/O 操作和范围查询的场景(如数据库索引)中, B+ 树可能是更好的选择。而在主要进行内存操作, 且需要频繁进行插入和删除操作的场景(如 Redis)中, 跳表可能更有优势。
- Mysql为什么使用B+树而不是跳表? Mysql 数据库是持久化数据库, 即是存储到磁盘上的, 因此查询时要求更少磁盘 IO, 且 Mysql 是读多写少的场景较多, 显然 B+ 树更加适合Mysql。
- Redis 的 ZSet 为什么使用跳表而不是B+树? Redis 是内存存储, 不存在 IO 的瓶颈, 所以跳表的层数的耗时可以忽略不计, 而且插入数据时不需要开销以平衡数据结构(写多)。
五、Zset命令
- 添加/更新已存在的元素
ZADD key score member [score member ...]
- 返回指定成员分数
ZSCORE key member
- 返回指定成员排名
ZRANK key member