Redis Zset (有序集合) 用法指南 – wiki基地


Redis Zset (有序集合) 用法指南:从入门到精通

导语

在 Redis 的众多数据结构中,String、List、Set、Hash 是我们耳熟能详的基本类型。然而,Redis 还提供了一种功能强大且应用广泛的数据结构——Sorted Set (有序集合),简称 Zset。Zset 不仅继承了 Set 成员唯一性的特性,还为每个成员关联了一个称为“分数”(score)的浮点数,并根据分数对成员进行排序。这种特性使得 Zset 在排行榜、优先级队列、时间序列数据处理等场景中表现出色。

本文将深入探讨 Redis Zset 的概念、内部实现、核心命令、常见用法、性能考量以及一些高级特性,帮助读者全面掌握这一强大的数据结构。

一、什么是 Redis Zset (有序集合)?

Redis 有序集合(Sorted Set)是字符串的集合,与普通集合(Set)不同的是,有序集合的每个成员都关联了一个分数(score)。这个分数是一个浮点数,Redis 会根据分数对有序集合的成员进行排序。

核心特性总结:

  1. 成员唯一性: 与 Set 类似,有序集合的每个成员都是唯一的,不允许重复。
  2. 排序性: 所有成员都会根据其关联的分数从小到大进行排序。如果分数相同,则会根据成员的字典序(lexicographical order)进行排序。
  3. 可检索性: 可以通过分数范围或成员排名(rank)来获取成员列表。
  4. 键值对结构: 从 Redis 的角度看,有序集合是一个键(Key),其中包含了多个成员(Member)及其对应的分数(Score)。可以视为 Key -> { (Member1, Score1), (Member2, Score2), ... } 的结构。

与 List 的区别在于:List 是按照元素的插入顺序排序,可以通过索引访问;Zset 是按照分数排序,不能通过索引访问,但可以通过分数或排名范围访问。

与 Set 的区别在于:Set 是无序的,成员唯一;Zset 是有序的(按分数),成员唯一。

二、Zset 的内部实现

理解 Zset 的内部实现有助于我们更好地理解其性能特性。Redis 的 Zset 底层主要使用了两种数据结构:

  1. 跳跃表 (Skip List): 一种概率性数据结构,用于快速地按分数范围或排名查找成员。跳跃表可以实现 O(log N) 的平均时间复杂度来执行插入、删除和查找操作,这与平衡树(如红黑树)相当,但实现起来更为简单。
  2. 哈希表 (Hash Table): 用于存储成员(Member)到其分数(Score)的映射,以及成员到其在跳跃表中节点的映射。这使得根据成员名称查找分数或执行其他成员相关操作(如 ZSCORE)非常快速,平均时间复杂度为 O(1)。

为什么同时使用两种结构?

  • 跳跃表保证了按分数排序和范围查找的高效性。
  • 哈希表保证了按成员名称查找分数或存在性的高效性。

通过结合使用,Redis Zset 能够同时支持按成员名称和按分数/排名进行高效操作。

底层优化:ziplist

对于成员数量较少且成员和分数都比较小的情况下,Redis 会使用一种更紧凑的线性数据结构 ziplist 来存储 Zset,以节省内存。当 Zset 满足特定条件(例如,成员数量小于 zset_max_ziplist_entries 配置,且单个成员或分数的大小小于 zset_max_ziplist_value 配置)时,会使用 ziplist。一旦超出这些限制,Redis 会自动将 Zset 转换为跳跃表和哈希表的组合结构。这是一种透明的优化,开发者通常无需关心,但理解它有助于解释为什么小 Zset 的内存开销相对较低。

三、Zset 核心命令详解

下面我们将详细介绍 Redis Zset 的常用命令及其用法。

1. 添加或更新成员:ZADD

ZADD key [NX|XX] [CH] [INCR] score member [score member ...]

  • 功能: 将一个或多个成员及其分数添加到有序集合中。如果成员已存在,则更新其分数。
  • 参数:
    • key: 有序集合的键名。
    • score: 成员的分数,必须是浮点数。
    • member: 成员的字符串值。
  • 选项:
    • NX: 只在成员不存在时添加。
    • XX: 只在成员已存在时更新。
    • CH: 修改返回值,使其返回新增成员和已存在但分数被更新的成员的总数,而不是只返回新增成员的数量。
    • INCR: 将成员的分数增加指定的 score 值。如果成员不存在,则添加该成员,其分数为 score。这个选项一次只能指定一个成员。
  • 返回值: 默认情况下,返回成功添加到有序集合中的新成员数量(不包括已存在但分数被更新的成员)。如果使用了 CH 选项,则返回新增成员和分数被更新成员的总数。如果使用了 INCR 选项,则返回成员更新后的新分数。
  • 时间复杂度: 对于每个要添加的成员,时间复杂度为 O(log N),其中 N 是有序集合中的成员数量。批量添加多个成员时,复杂度为 O(M log N),M 是要添加的成员数量。

示例:

“`bash

添加两个新成员

127.0.0.1:6379> ZADD myzset 10 “member1” 20 “member2”
(integer) 2

尝试添加已存在的成员,分数不变,使用 XX 选项

127.0.0.1:6379> ZADD myzset XX 20 “member2” 30 “member3” # member2 分数不变,member3 不会被添加
(integer) 0 # 只返回新增成员数量,member3 未添加

使用 CH 选项,并更新 member2 的分数

127.0.0.1:6379> ZADD myzset CH 25 “member2”
(integer) 1 # member2 分数被更新,返回 1

使用 INCR 选项增加 member1 的分数

127.0.0.1:6379> ZADD myzset INCR 5 “member1”
“15” # member1 的新分数是 10 + 5 = 15
“`

2. 获取有序集合的成员数量:ZCARD

ZCARD key

  • 功能: 返回有序集合中成员的数量。
  • 参数: key: 有序集合的键名。
  • 返回值: 有序集合的成员数量。如果键不存在,返回 0。
  • 时间复杂度: O(1)。

示例:

bash
127.0.0.1:6379> ZCARD myzset
(integer) 2 # 当前有序集合有 member1 和 member2 两个成员 (分数分别为 15 和 25)

3. 获取成员的分数:ZSCORE

ZSCORE key member

  • 功能: 返回有序集合中指定成员的分数。
  • 参数:
    • key: 有序集合的键名。
    • member: 要查询分数的成员。
  • 返回值: 成员的分数(字符串格式)。如果成员不存在或键不存在,返回 nil
  • 时间复杂度: O(1)。

示例:

“`bash
127.0.0.1:6379> ZSCORE myzset “member1”
“15”

127.0.0.1:6379> ZSCORE myzset “nonexistent_member”
(nil)
“`

4. 移除成员:ZREM

ZREM key member [member ...]

  • 功能: 从有序集合中移除一个或多个成员。
  • 参数:
    • key: 有序集合的键名。
    • member: 要移除的一个或多个成员。
  • 返回值: 成功移除的成员数量(不包括不存在的成员)。
  • 时间复杂度: 对于每个要移除的成员,时间复杂度为 O(log N)。批量移除多个成员时,复杂度为 O(M log N),M 是要移除的成员数量。

示例:

“`bash
127.0.0.1:6379> ZREM myzset “member1”
(integer) 1

127.0.0.1:6379> ZCARD myzset
(integer) 1 # 只剩 member2
“`

5. 按排名范围获取成员:ZRANGEZREVRANGE

这两个命令用于按成员在有序集合中的排名(索引)来获取成员列表。排名从 0 开始。

ZRANGE key start stop [WITHSCORES]

  • 功能: 返回有序集合中指定排名范围内的成员,按分数从小到大排序。
  • 参数:
    • key: 有序集合的键名。
    • start: 起始排名(包含)。
    • stop: 结束排名(包含)。
    • 排名可以是负数,-1 表示最后一个成员,-2 表示倒数第二个,以此类推。0 表示第一个成员。
  • 选项: WITHSCORES: 可选,如果指定,同时返回成员及其分数。
  • 返回值: 指定排名范围内的成员列表。如果指定 WITHSCORES,则返回成员及其分数的列表(交替出现)。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的成员数量。

ZREVRANGE key start stop [WITHSCORES]

  • 功能: 返回有序集合中指定排名范围内的成员,按分数从大到小排序。参数和选项与 ZRANGE 相同,但排名是反向计算的,0 表示分数最高的成员,1 表示分数次高的成员,以此类推。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的成员数量。

示例:

先添加一些成员:
ZADD myzset 10 m1 20 m2 30 m3 40 m4 50 m5
当前 myzset: (m1, 10), (m2, 20), (m3, 30), (m4, 40), (m5, 50)

“`bash

获取排名 0 到 2 的成员 (分数从小到大)

127.0.0.1:6379> ZRANGE myzset 0 2
1) “m1”
2) “m2”
3) “m3”

获取排名 -2 到 -1 的成员 (倒数第二个到最后一个)

127.0.0.1:6379> ZRANGE myzset -2 -1
1) “m4”
2) “m5”

获取所有成员及其分数

127.0.0.1:6379> ZRANGE myzset 0 -1 WITHSCORES
1) “m1”
2) “10”
3) “m2”
4) “20”
5) “m3”
6) “30”
7) “m4”
8) “40”
9) “m5”
10) “50”

获取排名 0 到 2 的成员 (分数从大到小)

127.0.0.1:6379> ZREVRANGE myzset 0 2
1) “m5”
2) “m4”
3) “m3”

获取所有成员及其分数 (分数从大到小)

127.0.0.1:6379> ZREVRANGE myzset 0 -1 WITHSCORES
1) “m5”
2) “50”
3) “m4”
4) “40”
5) “m3”
6) “30”
7) “m2”
8) “20”
9) “m1”
10) “10”
“`

6. 获取成员的排名:ZRANKZREVRANK

ZRANK key member

  • 功能: 返回有序集合中指定成员的排名(按分数从小到大排序,排名从 0 开始)。
  • 参数:
    • key: 有序集合的键名。
    • member: 要查询排名的成员。
  • 返回值: 成员的排名(整数)。如果成员不存在或键不存在,返回 nil
  • 时间复杂度: O(log N)。

ZREVRANK key member

  • 功能: 返回有序集合中指定成员的反向排名(按分数从大到小排序,排名从 0 开始)。
  • 参数:ZRANK
  • 返回值: 成员的反向排名(整数)。如果成员不存在或键不存在,返回 nil
  • 时间复杂度: O(log N)。

示例:

继续使用 myzset ((m1, 10), (m2, 20), (m3, 30), (m4, 40), (m5, 50))

“`bash

获取 m3 的排名 (分数从小到大)

127.0.0.1:6379> ZRANK myzset “m3”
(integer) 2 # m1 排名 0, m2 排名 1, m3 排名 2

获取 m3 的反向排名 (分数从大到小)

127.0.0.1:6379> ZREVRANK myzset “m3”
(integer) 2 # m5 排名 0, m4 排名 1, m3 排名 2
“`

7. 按分数范围获取成员:ZRANGEBYSCOREZREVRANGEBYSCORE

这两个命令是 Zset 最强大的功能之一,允许我们根据分数范围来检索成员。

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

  • 功能: 返回有序集合中分数在 minmax 范围内的成员,按分数从小到大排序。
  • 参数:
    • key: 有序集合的键名。
    • min: 最小分数。
    • max: 最大分数。
  • 分数范围的表示:
    • 包含:默认使用 minmax 值本身表示包含。例如 ZRANGEBYSCORE myzset 10 30 获取分数在 10 到 30(包含)之间的成员。
    • 不包含:在分数前加上 ( 表示不包含。例如 ZRANGEBYSCORE myzset (10 (30 获取分数大于 10 且小于 30 的成员。
    • 无穷大/小:使用 -inf 表示负无穷,+inf 表示正无穷。例如 ZRANGEBYSCORE myzset -inf +inf 获取所有成员。
  • 选项:
    • WITHSCORES: 可选,同时返回成员及其分数。
    • LIMIT offset count: 可选,限制返回结果的数量。offset 是跳过的成员数量,count 是最多返回的成员数量。用于分页。
  • 返回值: 指定分数范围内的成员列表。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的成员数量。

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

  • 功能: 返回有序集合中分数在 minmax 范围内的成员,按分数从大到小排序。注意 maxmin 的顺序与 ZRANGEBYSCORE 相反。
  • 参数:ZRANGEBYSCORE,但 max 在前,min 在后。
  • 选项:ZRANGEBYSCORE
  • 返回值: 指定分数范围内的成员列表,按分数从大到小排序。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的成员数量。

示例:

继续使用 myzset ((m1, 10), (m2, 20), (m3, 30), (m4, 40), (m5, 50))

“`bash

获取分数在 20 到 40 (包含) 之间的成员

127.0.0.1:6379> ZRANGEBYSCORE myzset 20 40
1) “m2”
2) “m3”
3) “m4”

获取分数大于 10 且小于等于 40 的成员及其分数

127.0.0.1:6379> ZRANGEBYSCORE myzset (10 40 WITHSCORES
1) “m2”
2) “20”
3) “m3”
4) “30”
5) “m4”
6) “40”

获取分数大于等于 20 且小于 50 的成员,并分页,跳过前 1 个,最多取 2 个

127.0.0.1:6379> ZRANGEBYSCORE myzset 20 (50 LIMIT 1 2 WITHSCORES
1) “m3”
2) “30”
3) “m4”
4) “40” # m2(20), m3(30), m4(40) 符合条件。跳过 m2,取 m3, m4。

获取分数在 20 到 40 (包含) 之间的成员 (分数从大到小)

127.0.0.1:6379> ZREVRANGEBYSCORE myzset 40 20
1) “m4”
2) “m3”
3) “m2”
“`

8. 统计分数范围内的成员数量:ZCOUNT

ZCOUNT key min max

  • 功能: 统计有序集合中分数在 minmax 范围内的成员数量。
  • 参数:
    • key: 有序集合的键名。
    • min: 最小分数。
    • max: 最大分数。
    • 分数范围的表示同 ZRANGEBYSCORE
  • 返回值: 指定分数范围内的成员数量。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是范围内的成员数量。在最坏情况下 (范围是整个集合),复杂度为 O(N)。

示例:

“`bash

统计分数在 20 到 40 (包含) 之间的成员数量

127.0.0.1:6379> ZCOUNT myzset 20 40
(integer) 3

统计分数大于 10 且小于 50 的成员数量

127.0.0.1:6379> ZCOUNT myzset (10 (50
(integer) 3 # m2, m3, m4
“`

9. 移除分数范围内的成员:ZREMRANGEBYSCORE

ZREMRANGEBYSCORE key min max

  • 功能: 移除有序集合中分数在 minmax 范围内的成员。
  • 参数:
    • key: 有序集合的键名。
    • min: 最小分数。
    • max: 最大分数。
    • 分数范围的表示同 ZRANGEBYSCORE
  • 返回值: 成功移除的成员数量。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是被移除的成员数量。

示例:

“`bash

移除分数在 20 到 40 (包含) 之间的成员

127.0.0.1:6379> ZREMRANGEBYSCORE myzset 20 40
(integer) 3

检查剩余成员

127.0.0.1:6379> ZRANGE myzset 0 -1 WITHSCORES
1) “m1”
2) “10”
3) “m5”
4) “50” # m2, m3, m4 已被移除
“`

10. 移除排名范围内的成员:ZREMRANGEBYRANK

ZREMRANGEBYRANK key start stop

  • 功能: 移除有序集合中指定排名范围内的成员,按分数从小到大排序。排名从 0 开始。
  • 参数:
    • key: 有序集合的键名。
    • start: 起始排名(包含)。
    • stop: 结束排名(包含)。
    • 排名可以是负数,意义同 ZRANGE
  • 返回值: 成功移除的成员数量。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是被移除的成员数量。

示例:

重置 myzset: ZADD myzset 10 m1 20 m2 30 m3 40 m4 50 m5

“`bash

移除排名 0 到 1 (即 m1, m2) 的成员

127.0.0.1:6379> ZREMRANGEBYRANK myzset 0 1
(integer) 2

检查剩余成员

127.0.0.1:6379> ZRANGE myzset 0 -1 WITHSCORES
1) “m3”
2) “30”
3) “m4”
4) “40”
5) “m5”
6) “50”
“`

11. 按字典序范围获取成员:ZRANGEBYLEX 等 (特殊用法)

当 Zset 中的所有成员分数都相同时(通常设置为 0),可以使用按字典序(Lexicographical Order)范围操作的命令。这主要用于实现类似数据库索引的功能,或者处理字符串范围查询。

ZRANGEBYLEX key min max [LIMIT offset count]

  • 功能: 返回有序集合中成员在指定字典序范围内的成员,成员必须具有相同的分数(通常为 0)。按字典序从小到大排序。
  • 参数:
    • key: 有序集合的键名。
    • min: 最小成员(字符串)。
    • max: 最大成员(字符串)。
  • 字典序范围的表示:
    • 包含:使用 [字符串 表示包含。例如 [apple
    • 不包含:使用 (字符串 表示不包含。例如 (banana
    • 无穷小:使用 - 表示字典序的最小值。
    • 无穷大:使用 + 表示字典序的最大值。
  • 选项: LIMIT offset count: 同 ZRANGEBYSCORE
  • 返回值: 指定字典序范围内的成员列表。
  • 时间复杂度: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的成员数量。

ZREVRANGEBYLEX key max min [LIMIT offset count]

  • 功能: 返回指定字典序范围内的成员,按字典序从大到小排序。注意 maxmin 的顺序与 ZRANGEBYLEX 相反。
  • 参数:ZRANGEBYLEX
  • 返回值: 指定字典序范围内的成员列表,按字典序从大到小排序。
  • 时间复杂度: O(log N + M)。

ZLEXCOUNT key min max

  • 功能: 统计字典序在 minmax 范围内的成员数量。
  • 参数:ZRANGEBYLEXminmax
  • 返回值: 指定字典序范围内的成员数量。
  • 时间复杂度: O(log N + M)。

ZREMPORTRANGEBYLEX key min max

  • 功能: 移除字典序在 minmax 范围内的成员。
  • 参数:ZRANGEBYLEXminmax
  • 返回值: 成功移除的成员数量。
  • 时间复杂度: O(log N + M)。

示例:

创建一个所有成员分数为 0 的 Zset:
ZADD lexset 0 apple 0 banana 0 cherry 0 date 0 fig
当前 lexset: (apple, 0), (banana, 0), (cherry, 0), (date, 0), (fig, 0) (按字典序排序)

“`bash

获取字典序在 [banana, date] 范围内的成员 (包含 banana 和 date)

127.0.0.1:6379> ZRANGEBYLEX lexset [banana [date
1) “banana”
2) “cherry”
3) “date”

获取字典序在 (apple, date) 范围内的成员 (不包含 apple 和 date)

127.0.0.1:6379> ZRANGEBYLEX lexset (apple (date
1) “banana”
2) “cherry”

获取字典序在 [banana, +) 范围内的前 2 个成员

127.0.0.1:6379> ZRANGEBYLEX lexset [banana + LIMIT 0 2
1) “banana”
2) “cherry”

统计字典序在 [banana, date] 范围内的成员数量

127.0.0.1:6379> ZLEXCOUNT lexset [banana [date
(integer) 3

移除字典序在 (cherry, +] 范围内的成员 (即 date, fig)

127.0.0.1:6379> ZREMPORTRANGEBYLEX lexset (cherry +
(integer) 2

检查剩余成员

127.0.0.1:6379> ZRANGE lexset 0 -1
1) “apple”
2) “banana”
3) “cherry”
``
注意:
ZRANGEBYLEX` 等命令要求所有成员具有相同的分数才能正确工作。如果分数不同,行为可能不稳定或与预期不符。

四、Zset 的集合操作

Redis 还提供了对多个 Zset 执行并集(Union)和交集(Intersection)操作,并将结果存储到新的键中。

1. 并集:ZUNIONSTORE

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

  • 功能: 计算给定多个有序集合的并集,并将结果存储到 destination 键中。如果 destination 已存在,会被覆盖。
  • 参数:
    • destination: 存储结果的键名。
    • numkeys: 输入的有序集合数量。
    • key [key ...]: 输入的有序集合键名列表。
  • 选项:
    • WEIGHTS weight [weight ...]: 可选,为每个输入有序集合的成员分数指定一个权重。计算并集时,成员的最终分数是其在每个输入集合中分数的加权和(如果成员存在于多个输入集合中)。默认权重是 1。权重的数量必须与 numkeys 相等。
    • AGGREGATE SUM|MIN|MAX: 可选,当一个成员同时出现在多个输入集合中时,如何计算其最终分数。
      • SUM (默认): 将所有分数相加。
      • MIN: 取所有分数中的最小值。
      • MAX: 取所有分数中的最大值。
  • 返回值: 结果有序集合中的成员数量。
  • 时间复杂度: O(N + M log M),其中 N 是所有给定有序集合成员数量的总和,M 是结果集合的成员数量。

示例:

创建两个 Zset:
ZADD set1 10 m1 20 m2 30 m3
ZADD set2 15 m1 25 m3 35 m4

“`bash

计算并集,结果存储到 set_union_sum (默认 SUM)

127.0.0.1:6379> ZUNIONSTORE set_union_sum 2 set1 set2
(integer) 4 # 结果集合有 m1, m2, m3, m4 四个成员

查看结果集合 (SUM 聚合): m1 (10+15=25), m2 (20), m3 (30+25=55), m4 (35)

127.0.0.1:6379> ZRANGE set_union_sum 0 -1 WITHSCORES
1) “m2”
2) “20”
3) “m1”
4) “25”
5) “m4”
6) “35”
7) “m3”
8) “55”

计算并集,使用 MIN 聚合

127.0.0.1:6379> ZUNIONSTORE set_union_min 2 set1 set2 AGGREGATE MIN
(integer) 4

查看结果集合 (MIN 聚合): m1 (min(10, 15)=10), m2 (20), m3 (min(30, 25)=25), m4 (35)

127.0.0.1:6379> ZRANGE set_union_min 0 -1 WITHSCORES
1) “m1”
2) “10”
3) “m2”
4) “20”
5) “m3”
6) “25”
7) “m4”
8) “35”
“`

2. 交集:ZINTERSTORE

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

  • 功能: 计算给定多个有序集合的交集,并将结果存储到 destination 键中。只有同时存在于所有输入集合中的成员才会被包含在结果集合中。
  • 参数:ZUNIONSTORE
  • 选项:ZUNIONSTORE。分数计算方式类似,只是只考虑出现在所有集合中的成员。
  • 返回值: 结果有序集合中的成员数量。
  • 时间复杂度: O(N log M),其中 N 是最小输入有序集合的成员数量,M 是所有给定有序集合成员数量的总和。或者更准确地说,取决于输入集合的大小和重叠度。

示例:

继续使用 set1 ((m1, 10), (m2, 20), (m3, 30)) 和 set2 ((m1, 15), (m3, 25), (m4, 35))

“`bash

计算交集,结果存储到 set_inter_sum (默认 SUM)

127.0.0.1:6379> ZINTERSTORE set_inter_sum 2 set1 set2
(integer) 2 # 只有 m1 和 m3 同时存在于两个集合中

查看结果集合 (SUM 聚合): m1 (10+15=25), m3 (30+25=55)

127.0.0.1:6379> ZRANGE set_inter_sum 0 -1 WITHSCORES
1) “m1”
2) “25”
3) “m3”
4) “55”
“`

3. 差集:ZDIFFSTORE (Redis 6.2+)

ZDIFFSTORE destination numkeys key [key ...]

  • 功能: 计算给定第一个有序集合与后续有序集合的差集,并将结果存储到 destination 键中。结果集合包含所有存在于第一个有序集合中,但不存在于任何后续有序集合中的成员。
  • 参数:
    • destination: 存储结果的键名。
    • numkeys: 输入的有序集合数量。
    • key [key ...]: 输入的有序集合键名列表。第一个键是基准集合。
  • 返回值: 结果有序集合中的成员数量。
  • 时间复杂度: O(N),其中 N 是所有给定有序集合成员数量的总和。

示例:

继续使用 set1 ((m1, 10), (m2, 20), (m3, 30)) 和 set2 ((m1, 15), (m3, 25), (m4, 35))

“`bash

计算 set1 和 set2 的差集 (set1 – set2),结果存储到 set_diff

127.0.0.1:6379> ZDIFFSTORE set_diff 2 set1 set2
(integer) 1 # m2 存在于 set1 但不存在于 set2

查看结果集合: 只有 m2 (来自 set1 的分数)

127.0.0.1:6379> ZRANGE set_diff 0 -1 WITHSCORES
1) “m2”
2) “20”
“`

五、Zset 的常见应用场景

Zset 的排序和范围查询特性使其非常适合处理需要根据数值进行排序和检索的场景。

  1. 排行榜 (Leaderboards):

    • 实现: 使用 Zset 的键存储排行榜,成员是玩家 ID 或用户名,分数是玩家的得分。
    • ZADD 更新玩家得分(如果玩家已存在)。
    • ZREVRANGE 获取前 N 名玩家列表(按分数从高到低)。
    • ZRANKZREVRANK 查询某个玩家的排名。
    • ZSCORE 查询玩家得分。
    • ZREMRANGEBYRANKZREMRANGEBYSCORE 清理旧数据或淘汰低分玩家。
    • ZCARD 获取参与排名的玩家总数。
  2. 优先级队列 (Priority Queues):

    • 实现: 使用 Zset 键存储任务队列,成员是任务 ID,分数是任务的优先级(数字越小优先级越高)或执行时间戳。
    • ZADD 添加任务。分数可以是优先级值,也可以是未来的时间戳,表示任务何时应该被处理。
    • ZRANGEBYSCORE key -inf current_timestamp LIMIT 0 1 (或 N 个) 查找当前需要处理的、分数(时间戳)小于等于当前时间的任务。
    • 使用 Lua 脚本或 Redis 事务结合 ZRANGEBYSCOREZREM 实现原子性的“获取并移除”操作,避免多个消费者获取同一个任务。
  3. 时间序列数据索引:

    • 实现: Zset 可以用来存储按时间排序的事件 ID 或数据点的索引。成员是事件 ID,分数是事件发生的时间戳。
    • ZADD 添加事件。
    • ZRANGEBYSCORE key start_timestamp end_timestamp 查询某个时间范围内发生的事件。
    • ZCOUNT key start_timestamp end_timestamp 统计某个时间范围内的事件数量。
    • ZREMRANGEBYSCORE key -inf old_timestamp 移除旧的过期事件。
  4. 范围查找索引:

    • 实现: 如果你需要根据数值字段(如价格、年龄、距离等)进行范围查找,可以将这些字段值作为 Zset 的分数,对应的记录 ID 作为成员。
    • ZADD 添加记录。
    • ZRANGEBYSCORE key min_value max_value 查找值在指定范围内的记录 ID。
    • ZCOUNT key min_value max_value 统计值在指定范围内的记录数量。
  5. 基于地理位置的附近查找 (结合 RedisGears 或客户端逻辑):
    虽然 Redis 有专门的 Geo 类型,但在某些简化场景下,如果只是需要查找某个分数范围内的成员(例如,以某个点为中心,距离在一个范围内的点),可以将距离计算结果作为 Zset 的分数。但是 Redis 的 Geo 类型(基于 Sorted Set 实现)提供了更专业的地理空间索引和查询功能,通常更推荐使用 Geo 类型。

六、性能考量与最佳实践

  • 时间复杂度: 大多数 Zset 操作(如 ZADD, ZREM, ZSCORE, ZRANK, ZRANGE, ZCOUNT, ZRANGEBYSCORE 等)的平均时间复杂度是 O(log N + M) 或 O(log N),其中 N 是 Zset 的大小,M 是操作涉及到的元素数量。这使得 Zset 在处理大量数据时仍然保持较高的性能。需要注意的是,ZRANGEZRANGEBYSCORE 返回大量元素时,M 会很高,整体复杂度也会随之增加。
  • 内存使用: Zset 使用跳跃表和哈希表,相比 Set 或 List 可能会占用更多的内存,尤其是在存储大量小成员时(因为每个成员都需要额外的空间来存储分数和跳跃表指针)。小 Zset 的 ziplist 编码会节省内存,但当数据量增长到一定阈值后会自动转换为标准结构。如果内存是瓶颈,可以考虑分片或使用其他更节省内存的数据结构(如果业务需求允许)。
  • 批量操作: 对于批量添加或移除成员,ZADDZREM 都支持变长参数,一次操作多个成员通常比循环多次单成员操作更高效。
  • 范围查询限制: 当使用 ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE 返回大量成员时,会消耗较多网络带宽和 Redis CPU 资源。如果只需要部分结果,务必使用 LIMIT 选项进行分页。
  • 字典序操作: ZRANGEBYLEX 等命令只在成员分数相同的情况下有效。在使用这些命令时,确保所有相关成员的分数是一致的。
  • 分数精度: Zset 的分数是双精度浮点数,能够表示很大的范围和较高的精度。但浮点数计算可能存在微小的精度问题,在依赖精确分数比较的场景需要注意。
  • 集合操作: ZUNIONSTOREZINTERSTORE 是阻塞命令,执行时间与输入集合的总大小成正比。在处理大型集合时,可能会导致 Redis 实例的阻塞。应谨慎使用,或者考虑将这些操作放到后台任务中执行。

七、总结

Redis 有序集合(Zset)是 Redis 提供的一种非常实用的数据结构,它结合了 Set 的成员唯一性和按分数排序的能力。通过跳跃表和哈希表的底层实现,Zset 能够在 O(log N) 的时间复杂度下实现高效的添加、删除、查询以及按分数或排名进行范围检索。这使得它在构建排行榜、优先级队列、时间索引以及其他需要有序、唯一元素并支持范围查询的场景中成为首选。

熟练掌握 Zset 的核心命令和特性,理解其内部工作原理和性能考量,将极大地提升你在使用 Redis 解决实际问题的能力。希望本文为你提供了全面而深入的 Zset 使用指南。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部