Redis ZSET入门指南:基础知识与实战示例

Redis ZSET 入门指南:基础知识与实战示例

Redis 的有序集合(Sorted Set,简称 ZSET)是一种独特的数据结构,它结合了集合(Set)和哈希表(Hash)的特性。ZSET 中的每个成员都关联着一个浮点数分值(score),Redis 通过这个分值对成员进行排序。这使得 ZSET 不仅可以像普通集合一样存储唯一元素,还能进行基于分值的排序和范围查询,非常适合实现排行榜、优先级队列、带权重的集合等场景。

本文将深入浅出地介绍 Redis ZSET 的基础知识、常用命令、内部实现原理,并通过丰富的实战示例,帮助您快速掌握 ZSET 的使用技巧,并在实际项目中灵活应用。

1. ZSET 基础知识

1.1. 什么是 ZSET?

ZSET 是一种有序的、不重复的元素集合。 它的特点可以概括为:

  • 有序性 (Ordered): ZSET 中的元素根据其关联的分值 (score) 进行排序。分值可以是整数或双精度浮点数。
  • 唯一性 (Unique): ZSET 中的元素是唯一的,不允许重复。
  • 关联分值 (Score): 每个元素都关联一个分值,用于排序和范围查询。

1.2. ZSET 的核心概念

  • 成员 (Member): ZSET 中的每个元素称为成员,它是唯一的字符串。
  • 分值 (Score): 与每个成员关联的浮点数值,用于排序。
  • 排名 (Rank): 元素在 ZSET 中根据分值排序后的位置,从 0 开始。

1.3. ZSET 与其它数据结构的比较

| 数据结构 | 元素是否唯一 | 元素是否有序 | 主要用途 |
| :--------- | :----------- | :----------- | :----------------------------------------- |
| 列表 (List) | 否 | 是 | 消息队列、时间线等 |
| 集合 (Set) | 是 | 否 | 去重、共同好友等 |
| ZSET | 是 | 是 | 排行榜、优先级队列、带权重的集合等 |
| 哈希 (Hash) | - | - | 存储对象属性 |

2. ZSET 常用命令

ZSET 提供了一系列丰富的命令,用于添加、删除、查询、修改成员及其分值。下面介绍一些最常用的命令:

2.1. 添加元素

  • ZADD key score member [score member ...]

    向 ZSET 中添加一个或多个成员,并指定其分值。如果成员已存在,则更新其分值。

    ZADD leaderboard 100 user1 90 user2 80 user3 # 添加三个成员及其分值
    ZADD leaderboard 95 user2 # 更新 user2 的分值为 95

2.2. 获取成员数量

  • ZCARD key

    获取 ZSET 中成员的数量。

    ZCARD leaderboard # 返回 leaderboard 中的成员数量

2.3. 获取成员分值

  • ZSCORE key member

    获取 ZSET 中指定成员的分值。

    ZSCORE leaderboard user1 # 返回 user1 的分值

2.4. 获取成员排名

  • ZRANK key member

    获取 ZSET 中指定成员的排名(从小到大)。排名从 0 开始。

    ZRANK leaderboard user2 # 返回 user2 的排名

  • ZREVRANK key member

    获取 ZSET 中指定成员的排名(从大到小)。排名从 0 开始。

    ZREVRANK leaderboard user2 # 返回 user2 的倒序排名

2.5. 获取指定排名范围的成员

  • ZRANGE key start stop [WITHSCORES]

    获取 ZSET 中指定排名范围内的成员(从小到大)。可以携带 WITHSCORES 选项以同时返回成员的分值。

    ZRANGE leaderboard 0 2 # 返回排名 0 到 2 的成员
    ZRANGE leaderboard 0 -1 WITHSCORES # 返回所有成员及其分值

  • ZREVRANGE key start stop [WITHSCORES]

    获取 ZSET 中指定排名范围内的成员(从大到小)。可以携带 WITHSCORES 选项以同时返回成员的分值。

    ZREVRANGE leaderboard 0 2 WITHSCORES # 返回倒序排名 0 到 2 的成员及其分值

2.6. 获取指定分值范围的成员

  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

    获取 ZSET 中指定分值范围内的成员(从小到大)。
    * min: 最小分值,可以使用 -inf 表示负无穷。
    * max: 最大分值,可以使用 +inf 表示正无穷。
    * WITHSCORES: 可选,返回成员及其分值。
    * LIMIT offset count: 可选,限制返回结果的数量和偏移量。

    ZRANGEBYSCORE leaderboard 80 100 # 返回分值在 80 到 100 之间的成员
    ZRANGEBYSCORE leaderboard -inf 90 WITHSCORES # 返回分值小于等于 90 的成员及其分值
    ZRANGEBYSCORE leaderboard 90 +inf LIMIT 0 5 # 返回分值大于等于 90 的前 5 个成员

  • ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
    获取指定分值范围的成员, 按分值从大到小排序

2.7. 删除成员

  • ZREM key member [member ...]

    从 ZSET 中删除一个或多个成员。

    ZREM leaderboard user3 # 删除 user3

  • ZREMRANGEBYRANK key start stop

    删除指定排名范围内的成员。

    ZREMRANGEBYRANK leaderboard 0 1 # 删除排名 0 和 1 的成员

  • ZREMRANGEBYSCORE key min max

    删除指定分值范围内的成员。

    ZREMRANGEBYSCORE leaderboard 80 90 # 删除分值在 80 到 90 之间的成员

2.8. 增加成员分值

  • ZINCRBY key increment member

    为 ZSET 中指定成员的分值增加一个增量。增量可以是正数或负数。

    ZINCRBY leaderboard 5 user1 # 为 user1 的分值增加 5
    ZINCRBY leaderboard -2 user2 # 为 user2 的分值减少 2

2.9 计算给定的一个或多个有序集的交集

  • ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

    计算给定的一个或多个有序集的交集并将结果集存储在新的有序集中。

    • destination: 新的有序集
    • numkeys:需要做交集的key个数
    • weights: 每个有序集的权重。默认为1.
    • AGGREGATE: 可以是 SUM, MIN, 或 MAX. 决定了交集里分值如何被计算. 默认 SUM.

2.10 计算给定的一个或多个有序集的并集

  • ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

计算给定的一个或多个有序集的并集并将结果集存储在新的有序集中。

3. ZSET 内部实现原理 (简述)

Redis ZSET 的底层实现采用了两种数据结构的组合:

  1. 跳跃表 (Skip List): 跳跃表是一种概率性的数据结构,它在链表的基础上增加了多层索引,以实现快速的查找、插入和删除操作。ZSET 利用跳跃表来实现按分值排序的功能。

  2. 哈希表 (Hash Table): 哈希表用于存储成员到分值的映射关系。通过哈希表,可以快速地根据成员查找其对应的分值。

ZSET 之所以选择跳跃表和哈希表的组合,是为了兼顾以下几个方面的性能:

  • 查找效率: 哈希表可以在 O(1) 时间复杂度内查找成员的分值,跳跃表可以在 O(logN) 时间复杂度内根据分值查找成员。
  • 插入和删除效率: 跳跃表的插入和删除操作的时间复杂度为 O(logN)。
  • 范围查询效率: 跳跃表支持高效的范围查询,可以在 O(logN + M) 时间复杂度内获取指定范围内的成员,其中 M 为范围内的成员数量。
  • 内存占用: 跳跃表相对于平衡树等数据结构,实现更简单,内存占用也更少。

当元素数量较少且每个元素的大小较小时,Redis 使用一种称为 ziplist 的紧凑编码方式来存储 ZSET,以节省内存。当元素数量或大小超过一定阈值时,Redis 会自动将 ziplist 转换为跳跃表和哈希表的组合。

4. ZSET 实战示例

4.1. 实现排行榜

排行榜是 ZSET 最典型的应用场景之一。例如,我们可以使用 ZSET 来实现一个游戏积分排行榜:

```

添加玩家及其积分

ZADD game_leaderboard 1000 player1 800 player2 1200 player3 900 player4

获取排行榜前三名

ZREVRANGE game_leaderboard 0 2 WITHSCORES

获取玩家的排名

ZREVRANK game_leaderboard player2

更新玩家的积分

ZINCRBY game_leaderboard 100 player1

获取积分在 900 到 1100 之间的玩家

ZRANGEBYSCORE game_leaderboard 900 1100 WITHSCORES

删除积分低于 800 的玩家

ZREMRANGEBYSCORE game_leaderboard -inf 800
```

4.2. 实现优先级队列

ZSET 可以用于实现优先级队列,其中分值表示任务的优先级。

```

添加任务及其优先级

ZADD task_queue 1 task1 2 task2 3 task3

获取优先级最高的任务

ZREVRANGE task_queue 0 0

处理任务后,将其从队列中删除

ZREM task_queue task3

调整任务的优先级

ZINCRBY task_queue -1 task2 # 将 task2 的优先级提高
```

4.3. 实现带权重的集合

ZSET 可以用于实现带权重的集合,其中分值表示元素的权重。例如,我们可以使用 ZSET 来实现一个推荐系统,其中分值表示推荐商品的权重。

```

添加商品及其权重

ZADD recommended_products 0.8 product1 0.9 product2 0.7 product3

获取权重最高的几个商品

ZREVRANGE recommended_products 0 2 WITHSCORES

根据用户的行为调整商品权重

ZINCRBY recommended_products 0.1 product1 # 用户点击了 product1,增加其权重
```

4.4 实现延时队列

可以把需要执行的任务和任务触发时间作为 score 存到 ZSET 中,然后通过轮询的方式,定时去扫描 ZSET,找到触发时间小于等于当前时间的任务执行。

```

添加延时任务,触发时间为 Unix 时间戳

ZADD delayed_tasks 1678886400 task1 1678890000 task2 1678893600 task3

定时扫描并执行到期的任务

假设当前时间为 current_time

ZRANGEBYSCORE delayed_tasks -inf current_time

获取到期的任务列表后,执行任务并从 ZSET 中删除已执行的任务

```

4.5 实现时间轴

可以将发布时间作为 score, 内容作为 member。

```python

添加内容

ZADD timeline 1678886400 "post1" 1678890000 "post2" 1678893600 "post3"

获取最近发布的 10 条内容

ZREVRANGE timeline 0 9
```

5. ZSET 的注意事项

  • 分值精度: ZSET 的分值使用双精度浮点数表示,可能会存在精度问题。在需要精确表示分值的场景下,需要注意精度损失的可能性。
  • 成员长度: ZSET 的成员是字符串,长度不宜过长,否则会影响性能和内存占用。
  • 避免大key: 过多的成员数量也会影响性能, 需要定期清理.

6. ZSET 的进阶使用

6.1. Lua 脚本

可以将多个 ZSET 操作组合成一个 Lua 脚本,以原子方式执行,提高效率并保证数据一致性。

6.2. Redis 集群

在 Redis 集群环境下,ZSET 的操作需要注意键的分布。可以使用哈希标签 (hash tag) 将相关的键分配到同一个节点上,以提高性能。

7. 更进一步

本文详细介绍了 Redis ZSET 的基础知识、常用命令和实战示例,为您提供了入门 ZSET 的全面指南。ZSET 是一种功能强大且灵活的数据结构,熟练掌握 ZSET 可以帮助您解决各种实际问题。

希望您通过阅读这篇文章可以真正理解并开始使用Redis 的 ZSET!

THE END