Redis 有序集合 (Zset) 使用教程详解
Redis 不仅支持简单的数据结构如字符串、列表、哈希和集合,还提供了一种非常强大的复合数据结构:有序集合(Sorted Set),在 Redis 中通常称为 Zset。Zset 结合了集合(Set)的唯一性特点和哈希(Hash)的关联性特点,同时为每个成员(Member)关联了一个分数(Score),并根据这个分数对成员进行排序。这使得 Zset 在实现各种排行榜、带有优先级的任务队列、需要按权重或时间排序的数据存储等场景中表现出色。
本文将详细介绍 Redis Zset 的核心概念、常用命令以及典型应用场景,帮助你深入理解和掌握 Zset 的使用。
1. Zset 的核心概念
一个 Zset 数据结构包含以下几个关键组成部分:
- 键 (Key):一个 Zset 在 Redis 中由一个唯一的键名标识,就像其他所有 Redis 数据结构一样。
- 成员 (Member):有序集合中的每个元素被称为成员。成员是唯一的,就像 Redis 的 Set 一样,不允许有重复的成员。成员通常是字符串类型。
- 分数 (Score):与每个成员关联的一个浮点数值。Zset 的核心特性就在于根据这个分数对成员进行排序。分数可以是整数或双精度浮点数。
Zset 的特点总结:
- 唯一性:同一个 Zset 中的成员是唯一的。
- 有序性:Zset 中的成员根据其关联的分数进行排序,分数低的成员排在前面,分数高的成员排在后面。如果两个成员的分数相同,则按成员的字典序( lexicographical order)进行排序。
- 可查找性:可以通过成员快速查找其对应的分数,也可以通过分数或排名范围查找成员。
2. Zset 的常用命令详解
掌握 Zset 的使用主要依赖于理解和运用其提供的命令。下面我们将介绍一些最常用和关键的 Zset 命令。
2.1 添加/更新成员:ZADD
ZADD
命令用于向有序集合中添加一个或多个成员及其分数。如果成员已经存在,则会更新其分数并重新排序。
redis
ZADD key score1 member1 [score2 member2 ...] [NX | XX] [CH] [INCR]
key
: 有序集合的键名。score
: 成员的分数,浮点数。member
: 成员的值,字符串。NX
: 只在成员不存在时添加。XX
: 只在成员存在时更新。CH
: 返回值从新添加成员的数量变为被改变(新添加或分数被更新)成员的总数。INCR
: 将成员的分数加上指定的分数(分数可以是负数,实现减法)。这个选项只能用于一次添加一个成员。
示例:
“`redis
添加多个成员
ZADD myzset 10 “memberA” 20 “memberB” 5 “memberC”
输出:3 (表示成功添加了3个成员)
更新成员A的分数
ZADD myzset 15 “memberA”
输出:0 (因为是更新,不是新增,如果使用了CH选项,会返回1)
尝试添加已存在的成员,但不更新分数 (使用 NX)
ZADD myzset 25 “memberA” NX
输出:0 (因为memberA已存在)
尝试更新不存在的成员 (使用 XX)
ZADD myzset 30 “memberD” XX
输出:0 (因为memberD不存在)
使用 INCR 增加成员B的分数
ZADD myzset INCR 5 “memberB”
输出:25.0 (memberB的新分数为20+5=25)
“`
2.2 获取成员排名(按分数从小到大):ZRANK
ZRANK
命令用于获取指定成员在有序集合中的排名(从0开始)。排名按分数从小到大计算。
redis
ZRANK key member
key
: 有序集合的键名。member
: 要查询排名的成员。
示例:
承接上例,myzset
中成员按分数排序(从小到大)为:memberC(5)
-> memberA(15)
-> memberB(25)
。
“`redis
ZRANK myzset “memberC”
输出:0 (memberC是排名第一,索引为0)
ZRANK myzset “memberA”
输出:1 (memberA是排名第二,索引为1)
ZRANK myzset “memberB”
输出:2 (memberB是排名第三,索引为2)
ZRANK myzset “memberX”
输出:(nil) (memberX不存在)
“`
2.3 获取成员排名(按分数从大到小):ZREVRANK
ZREVRANK
命令与 ZRANK
类似,但排名按分数从大到小计算(最高分排名为0)。
redis
ZREVRANK key member
key
: 有序集合的键名。member
: 要查询排名的成员。
示例:
承接上例,myzset
中成员按分数排序(从大到小)为:memberB(25)
-> memberA(15)
-> memberC(5)
。
“`redis
ZREVRANK myzset “memberB”
输出:0 (memberB是最高分,排名为0)
ZREVRANK myzset “memberA”
输出:1
ZREVRANK myzset “memberC”
输出:2
“`
2.4 按排名范围获取成员:ZRANGE
ZRANGE
命令用于按排名(索引)范围获取有序集合中的成员。排名按分数从小到大计算。
redis
ZRANGE key start stop [WITHSCORES]
key
: 有序集合的键名。start
: 开始索引(0为第一个成员)。stop
: 结束索引。start
和stop
都可以使用负数表示从末尾开始计数,-1
表示最后一个成员,-2
表示倒数第二个,以此类推。范围是闭区间,包含start
和stop
对应的成员。WITHSCORES
: 可选参数,如果指定,返回结果会包含成员及其对应的分数。
示例:
承接上例,myzset
按分数从小到大:memberC(5)
-> memberA(15)
-> memberB(25)
。
“`redis
ZRANGE myzset 0 2
输出:1) “memberC” 2) “memberA” 3) “memberB” (获取所有成员)
ZRANGE myzset 0 1 WITHSCORES
输出:1) “memberC” 2) “5” 3) “memberA” 4) “15” (获取排名0到1的成员及其分数)
ZRANGE myzset -2 -1
输出:1) “memberA” 2) “memberB” (获取倒数第2到倒数第1的成员)
“`
2.5 按排名范围获取成员(从大到小):ZREVRANGE
ZREVRANGE
命令与 ZRANGE
类似,但按分数从大到小获取成员范围。
redis
ZREVRANGE key start stop [WITHSCORES]
参数含义与 ZRANGE
相同,但索引是针对从大到小排序的列表。
示例:
承接上例,myzset
按分数从大到小:memberB(25)
-> memberA(15)
-> memberC(5)
。
“`redis
ZREVRANGE myzset 0 2
输出:1) “memberB” 2) “memberA” 3) “memberC” (获取所有成员,从最高分到最低分)
ZREVRANGE myzset 0 1 WITHSCORES
输出:1) “memberB” 2) “25” 3) “memberA” 4) “15” (获取排名0到1的成员及其分数)
“`
2.6 按分数范围获取成员:ZRANGEBYSCORE
ZRANGEBYSCORE
命令是 Zset 中非常强大且常用的命令,它允许你根据成员的分数范围来获取成员。
redis
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
key
: 有序集合的键名。min
: 最小分数。max
: 最大分数。WITHSCORES
: 可选参数,返回结果包含分数。LIMIT offset count
: 可选参数,用于分页。从匹配结果的offset
位置开始,获取count
个成员。
min
和 max
可以是具体的分数,也可以是 -inf
(负无穷) 或 +inf
(正无穷)。默认情况下,范围是闭区间(包含等于 min
和 max
的成员)。可以使用前缀 (
表示开区间(不包含等于该值的成员)。
示例:
承接上例,myzset
按分数从小到大:memberC(5)
-> memberA(15)
-> memberB(25)
。
“`redis
获取分数在10到20之间的成员 (包含10和20)
ZRANGEBYSCORE myzset 10 20
输出:1) “memberA”
获取分数大于5小于25的成员 (开区间)
ZRANGEBYSCORE myzset (5 (25
输出:1) “memberA”
获取分数小于等于15的所有成员 (包含负无穷和15)
ZRANGEBYSCORE myzset -inf 15 WITHSCORES
输出:1) “memberC” 2) “5” 3) “memberA” 4) “15”
获取分数大于等于15的所有成员 (包含15和正无穷)
ZRANGEBYSCORE myzset 15 +inf
输出:1) “memberA” 2) “memberB”
获取分数大于5的所有成员,只取前1个 (分页)
ZRANGEBYSCORE myzset (5 +inf LIMIT 0 1
输出:1) “memberA”
“`
2.7 按分数范围获取成员(从大到小):ZREVRANGEBYSCORE
ZREVRANGEBYSCORE
命令与 ZRANGEBYSCORE
类似,但按分数从大到小获取成员范围。注意 max
参数在前,min
参数在后。
redis
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
示例:
承接上例,myzset
按分数从大到小:memberB(25)
-> memberA(15)
-> memberC(5)
。
“`redis
获取分数在20到10之间的成员 (包含20和10),注意这里是 max min 的顺序
ZREVRANGEBYSCORE myzset 20 10
输出:1) “memberA”
获取分数小于25大于5的成员 (开区间)
ZREVRANGEBYSCORE myzset (25 (5
输出:1) “memberA”
“`
2.8 获取成员分数:ZSCORE
ZSCORE
命令用于获取指定成员的分数。
redis
ZSCORE key member
key
: 有序集合的键名。member
: 要查询分数的成员。
示例:
“`redis
ZSCORE myzset “memberA”
输出:”15″ (返回分数的字符串表示)
ZSCORE myzset “nonexistent”
输出:(nil) (成员不存在)
“`
2.9 移除成员:ZREM
ZREM
命令用于从有序集合中移除一个或多个成员。
redis
ZREM key member [member ...]
key
: 有序集合的键名。member
: 要移除的成员。
示例:
“`redis
ZREM myzset “memberC”
输出:1 (表示成功移除了1个成员)
ZREM myzset “memberA” “memberX”
输出:1 (memberA被移除,memberX不存在,所以总共移除1个)
“`
2.10 获取成员数量:ZCARD
ZCARD
命令用于获取有序集合中的成员数量。
redis
ZCARD key
key
: 有序集合的键名。
示例:
“`redis
ZCARD myzset
输出:1 (在移除了 memberC 和 memberA 后,myzset 只剩下 memberB)
“`
2.11 统计分数范围内的成员数量:ZCOUNT
ZCOUNT
命令用于统计分数在指定范围内的成员数量。
redis
ZCOUNT key min max
参数含义与 ZRANGEBYSCORE
中的 min
和 max
相同,支持开闭区间和无穷。
示例:
重新添加一些成员:ZADD myzset 10 "m1" 20 "m2" 30 "m3" 40 "m4" 50 "m5"
“`redis
ZCOUNT myzset 20 40
输出:3 (成员 m2, m3, m4 的分数在 [20, 40] 范围内)
ZCOUNT myzset (20 40
输出:2 (成员 m3, m4 的分数在 (20, 40] 范围内)
“`
2.12 按字典序范围获取成员:ZRANGEBYLEX
/ ZREVRANGEBYLEX
当多个成员拥有相同的分数时,它们会按成员的字典序进行排序。ZRANGEBYLEX
和 ZREVRANGEBYLEX
命令允许你按成员的字典序范围来获取成员。这些命令要求指定的成员范围必须 全部 具有相同的分数,或者在指定分数范围(通常是 -inf
到 +inf
)内,所有成员都必须具有相同的分数才能正确使用。它们通常用于对具有相同分数的成员进行进一步的范围查找,或者在所有成员分数都相同时,完全按字典序进行操作。
redis
ZRANGEBYLEX key min max [LIMIT offset count]
ZREVRANGEBYLEX key max min [LIMIT offset count]
min
,max
: 指定成员的字典序范围。可以使用[
表示闭区间,(
表示开区间。使用+
表示无穷大字符串,-
表示无穷小字符串。
示例:
假设我们有一个 Zset,所有成员的分数都是0:ZADD myzset 0 "apple" 0 "banana" 0 "cherry" 0 "date"
“`redis
ZRANGEBYLEX myzset [apple [cherry
输出:1) “apple” 2) “banana” 3) “cherry” (获取字典序在 [apple, cherry] 范围内的成员)
ZRANGEBYLEX myzset (banana +
输出:1) “cherry” 2) “date” (获取字典序大于 banana 的所有成员)
“`
2.13 获取并移除分数最低的成员:ZPOPMIN
ZPOPMIN
命令从有序集合中移除并返回分数最低的成员及其分数。可以指定数量,移除并返回多个分数最低的成员。
redis
ZPOPMIN key [count]
key
: 有序集合的键名。count
: 要移除并返回的成员数量。
示例:
“`redis
ZADD myzset 10 “m1” 20 “m2” 5 “m0” 30 “m3”
ZPOPMIN myzset
输出:1) “m0” 2) “5” (移除并返回 m0 及其分数 5)
ZPOPMIN myzset 2
输出:1) “m1” 2) “10” 3) “m2” 4) “20” (移除并返回分数最低的两个成员及其分数)
“`
2.14 获取并移除分数最高的成员:ZPOPMAX
ZPOPMAX
命令与 ZPOPMIN
类似,但移除并返回分数最高的成员及其分数。
redis
ZPOPMAX key [count]
示例:
“`redis
ZADD myzset 10 “m1” 20 “m2” 5 “m0” 30 “m3”
ZPOPMAX myzset
输出:1) “m3” 2) “30” (移除并返回 m3 及其分数 30)
“`
2.15 阻塞式获取并移除分数最低的成员:BZPOPMIN
BZPOPMIN
是 ZPOPMIN
的阻塞版本。当指定的有序集合为空时,连接将被阻塞,直到有成员被添加到其中一个有序集合,或者达到指定的超时时间。
redis
BZPOPMIN key [key ...] timeout
key [key ...]
: 一个或多个有序集合的键名。timeout
: 阻塞的秒数。0 表示永远阻塞。
返回值是包含三部分的数组:被弹出的成员来源的键名、被弹出的成员以及其分数。
2.16 阻塞式获取并移除分数最高的成员:BZPOPMAX
BZPOPMAX
是 ZPOPMAX
的阻塞版本。
redis
BZPOPMAX key [key ...] timeout
参数和返回值类似 BZPOPMIN
。
2.17 计算有序集合的并集和交集:ZUNIONSTORE
/ ZINTERSTORE
这些命令用于计算多个有序集合的并集或交集,并将结果存储到一个新的有序集合中。
redis
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
destination
: 存储结果的有序集合的键名。numkeys
: 输入的有序集合数量。key [key ...]
: 输入的有序集合的键名。WEIGHTS
: 可选参数,为每个输入集合指定一个乘法因子,成员的分数在聚合前会先乘以这个因子。AGGREGATE
: 可选参数,指定当一个成员出现在多个输入集合中时,如何计算其最终分数。SUM
(默认)将所有分数相加,MIN
取最小分数,MAX
取最大分数。
示例:
“`redis
ZADD zset1 10 “a” 20 “b”
ZADD zset2 15 “b” 25 “c” 30 “d”
计算并集,分数相加 (默认)
ZUNIONSTORE zset_union 2 zset1 zset2
结果 zset_union: “a”:10, “b”:35, “c”:25, “d”:30
计算交集,取最小分数
ZINTERSTORE zset_inter 2 zset1 zset2 AGGREGATE MIN
结果 zset_inter: “b”:15 (只有”b”同时存在,取 min(20, 15)=15)
计算并集,zset2 的分数乘2,分数相加
ZUNIONSTORE zset_union_weighted 2 zset1 zset2 WEIGHTS 1 2 AGGREGATE SUM
结果 zset_union_weighted: “a”:10, “b”:(20 + 152)=50, “c”:252=50, “d”:30*2=60
“`
3. Zset 的应用场景
Zset 的有序性和分数特性使其非常适合以下场景:
- 排行榜 (Rankings):这是 Zset 最典型的应用。例如,游戏玩家得分排行榜、网站文章点击量排行榜、商品销量排行榜等。分数就是得分、点击量或销量,成员是玩家ID、文章ID或商品ID。使用
ZADD
添加/更新得分,ZREVRANK
获取用户排名,ZREVRANGE
获取排行榜前N名。 - 优先级队列 (Priority Queues):将任务作为成员,优先级作为分数。使用
ZADD
添加任务,ZPOPMIN
获取并移除最高优先级(分数最低)的任务。BZPOPMIN
可以实现阻塞等待,适合作为生产者-消费者模型中的任务队列。 - 时间序列数据 (Time Series Data):将事件作为成员,时间戳作为分数。可以方便地按时间范围 (
ZRANGEBYSCORE
) 查询事件。 - 延迟队列 (Delayed Queue):将任务作为成员,执行时间戳作为分数。使用
ZRANGEBYSCORE
查询到达执行时间的任务 (-inf
到当前时间戳),然后使用ZREM
移除或ZPOPMIN
弹出执行。 - 带权重的标签/分类 (Weighted Tags/Categories):给内容打标签,标签本身是成员,权重是分数,表示标签与内容的关联度或重要性。
- 限流/滑动窗口 (Rate Limiting/Sliding Window):可以使用 Zset 存储某个用户在特定时间窗口内的操作记录(时间戳作为分数),然后使用
ZCOUNT
统计窗口内的操作次数进行限流判断。
4. Zset 的内部实现与性能
Redis Zset 的内部实现通常结合使用了跳跃表 (Skip List) 和哈希表 (Hash Table)。
- 哈希表 用于存储成员到分数的映射关系,这样可以通过成员 O(1) 的时间复杂度快速查找其分数。
- 跳跃表 用于存储成员到分数的映射关系,并按分数有序排列,这样可以在 O(log N) 的时间复杂度内完成范围查询、排名查询等操作。
当 Zset 中的元素数量较少或者成员字符串较短时,Redis 会采用一种更节省内存的紧凑结构 (ziplist 或 listpack)。当元素数量或成员大小超过一定阈值时,才会转换为跳跃表和哈希表的实现。
大多数 Zset 操作,如 ZADD
, ZRANK
, ZSCORE
, ZREM
, ZRANGE
, ZRANGEBYSCORE
的时间复杂度都是 O(log N) 或 O(log N + M)(M为返回的元素数量),这使得 Zset 在处理大量数据时依然能保持较高的性能。ZCARD
的时间复杂度是 O(1)。
5. 总结
Redis 的有序集合 (Zset) 是一个功能强大且灵活的数据结构,它通过为每个成员关联一个分数来实现有序存储,并提供了丰富的命令用于成员的添加、移除、查询(按排名或分数范围)、统计等操作。凭借其独特的有序性和高效的底层实现,Zset 在各种需要处理排序数据的应用场景中发挥着核心作用,尤其是在构建排行榜、优先级队列和时间序列相关功能时。理解 Zset 的核心概念和常用命令,将极大地扩展你在使用 Redis 进行应用开发时的能力。
开始实践吧!尝试使用 Redis CLI 或你喜欢的客户端库连接到 Redis,按照上面的命令示例操作 Zset,加深理解。