Redis数据结构:Zset类型全面解析

205 阅读7分钟

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), 如查找1008;

- skiplist一级索引:
  查找100, 遍历6次;

image.png

- skiplist二级索引:
  查找100, 遍历6次;

image.png

  • 插入元素18

    • 原数据

    image.png

    • 插入变动

image.png

四、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