Redis ZSET 常用命令详解与代码示例
Redis ZSET (有序集合) 常用命令详解与代码示例
引言
Redis (Remote Dictionary Server) 是一款基于内存的高性能键值存储系统,以其丰富的数据结构、出色的性能和广泛的应用场景而备受青睐。在 Redis 提供的多种数据结构中,有序集合(Sorted Set,简称 ZSET)是一种非常强大且独特的数据类型。它类似于常规的集合(Set),保证了成员(member)的唯一性,但同时,每个成员都关联着一个浮点数类型的分数(score)。Redis 正是根据这个分数对集合中的成员进行排序。这种特性使得 ZSET 在需要排序、范围查询、排行榜、优先级队列等场景下表现得极为出色。
本文旨在详细介绍 Redis ZSET 的常用命令,深入解析其用法、参数、返回值和时间复杂度,并结合实际的代码示例(主要使用 redis-cli
和 Python 的 redis-py
库),帮助开发者全面理解和高效使用 ZSET。
ZSET 核心概念
在深入命令之前,我们先回顾一下 ZSET 的几个核心概念:
- 成员 (Member):集合中的元素,必须是唯一的字符串。
- 分数 (Score):与每个成员关联的浮点数(double-precision floating-point number)。Redis 使用分数对成员进行排序。分数可以重复。
- 排序 (Ordering):成员默认按分数从小到大排序。如果分数相同,则按成员的字典序(lexicographical order)排序(具体取决于 Redis 版本和配置,但通常是这样)。
- 唯一性 (Uniqueness):成员在 ZSET 中是唯一的,但分数可以相同。
- 内部实现:ZSET 的底层实现通常结合了跳跃表(Skip List)和哈希表(Hash Table)。哈希表用于 O(1) 时间复杂度查找成员对应的分数,跳跃表则保证了在 O(log N) 时间复杂度内进行范围查询、排序等操作(N 为集合中的成员数量)。
ZSET 常用命令详解
我们将 ZSET 命令大致分为以下几类进行讲解:
- 添加与更新成员
- 查询成员与分数
- 范围查询(按排名、按分数、按字典序)
- 统计计算
- 删除成员
- 增量操作
- 集合运算(交集、并集)
- 迭代与弹出
1. 添加与更新成员
ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
- 作用:向有序集合
key
添加一个或多个成员,或者更新已存在成员的分数。 - 参数:
key
: 有序集合的键名。NX
: 仅在成员不存在时添加。不更新已存在成员的分数。XX
: 仅在成员已存在时更新分数。不添加新成员。GT
: (Redis 6.2+) 仅在新分数大于当前分数时才更新。隐含XX
行为。LT
: (Redis 6.2+) 仅在新分数小于当前分数时才更新。隐含XX
行为。CH
: (Changed) 返回值从“新添加成员的数量”变为“发生变化的成员总数”(包括添加和更新)。INCR
: 当指定此选项时,ZADD
的行为类似ZINCRBY
。此时只能指定一对score member
。它会将指定成员的分数增加score
值。如果成员不存在,则添加它,分数为score
。返回值是成员的新分数(字符串形式)。score member
: 要添加或更新的成员及其分数。可以指定多对。
- 返回值:
- 默认情况下,返回被成功添加的新成员数量(不包括仅更新分数的成员)。
- 如果使用了
CH
选项,返回此次操作导致变化的成员总数(添加 + 更新)。 - 如果使用了
INCR
选项,返回成员的新分数(字符串)。
- 时间复杂度:O(log N),其中 N 是有序集合的成员数。添加/更新每个成员都需要 O(log N)。
-
示例 (
redis-cli
):
```bash
# 添加成员
redis> ZADD leaderboard 1000 user:1
(integer) 1
redis> ZADD leaderboard 1500 user:2 900 user:3
(integer) 2更新成员分数
redis> ZADD leaderboard 1600 user:2
(integer) 0 # user:2 已存在,只更新分数,不算新添加使用 CH 选项
redis> ZADD leaderboard CH 1100 user:1 1700 user:4
(integer) 2 # user:1 被更新,user:4 被添加,共 2 个变化使用 NX 选项 (user:1 已存在,不会更新;user:5 不存在,会添加)
redis> ZADD leaderboard NX 1200 user:1 1800 user:5
(integer) 1 # 只添加了 user:5使用 XX 选项 (user:1 存在,会更新;user:6 不存在,不会添加)
redis> ZADD leaderboard XX 1300 user:1 1900 user:6
(integer) 0 # 虽然 user:1 被更新,但 XX 不影响返回值计算方式(除非用 CH)使用 INCR 选项 (类似 ZINCRBY)
redis> ZADD leaderboard INCR 50 user:1
"1350" # 返回 user:1 的新分数(Redis 6.2+) 使用 GT 选项 (1400 > 1350,更新)
redis> ZADD leaderboard GT 1400 user:1
(integer) 0 # 更新操作,默认返回0(Redis 6.2+) 使用 LT 选项 (1300 < 1400,更新)
redis> ZADD leaderboard LT CH 1300 user:1
(integer) 1 # 使用 CH,更新成功返回 1
* **示例 (Python `redis-py`)**:
python
import redisr = redis.Redis(decode_responses=True)
添加单个成员
r.zadd('leaderboard', {'user:1': 1000}) # 返回添加成功的数量: 1
添加多个成员
r.zadd('leaderboard', {'user:2': 1500, 'user:3': 900}) # 返回 2
更新并使用 CH
changed_count = r.zadd('leaderboard', {'user:1': 1100, 'user:4': 1700}, ch=True)
print(f"Changed members: {changed_count}") # 输出: Changed members: 2使用 NX
added_nx = r.zadd('leaderboard', {'user:1': 1200, 'user:5': 1800}, nx=True)
print(f"Added with NX: {added_nx}") # 输出: Added with NX: 1使用 INCR
new_score = r.zadd('leaderboard', {'user:1': 50}, incr=True)
print(f"New score for user:1 after INCR: {new_score}") # 输出: New score for user:1 after INCR: 1150.0 (注意是 float)(需要 Redis 6.2+ 和较新 redis-py) 使用 GT/LT (示例语法可能略有不同,需查阅库文档)
r.zadd('leaderboard', {'user:1': 1200}, gt=True, ch=True)
r.zadd('leaderboard', {'user:1': 1000}, lt=True, ch=True)
```
2. 查询成员与分数
ZSCORE key member
- 作用:获取有序集合
key
中指定成员member
的分数。 - 返回值:成员的分数(字符串形式)。如果成员不存在或 key 不存在,返回
nil
。 - 时间复杂度:O(1)
- 示例 (
redis-cli
):
bash
redis> ZSCORE leaderboard user:1
"1150"
redis> ZSCORE leaderboard non_existent_user
(nil) -
示例 (Python
redis-py
):
```python
score = r.zscore('leaderboard', 'user:1')
if score is not None:
print(f"Score of user:1: {float(score)}") # 输出: Score of user:1: 1150.0
else:
print("user:1 not found.")score_non_exist = r.zscore('leaderboard', 'non_existent_user')
print(f"Score of non_existent_user: {score_non_exist}") # 输出: Score of non_existent_user: None
```
ZMScore key member [member ...]
(Redis 6.2+)
- 作用:获取有序集合
key
中一个或多个指定成员的分数。 - 返回值:一个分数列表,顺序与请求的成员顺序相同。如果某个成员不存在,对应位置返回
nil
。 - 时间复杂度:O(M),其中 M 是请求的成员数量。
- 示例 (
redis-cli
):
bash
redis> ZMSCORE leaderboard user:1 user:3 non_existent_user user:2
1) "1150"
2) "900"
3) (nil)
4) "1600" - 示例 (Python
redis-py
):
python
scores = r.zmscore('leaderboard', ['user:1', 'user:3', 'non_existent_user', 'user:2'])
print(f"Scores: {scores}") # 输出: Scores: [1150.0, 900.0, None, 1600.0]
# 注意 redis-py 可能会自动将数字字符串转为 float
3. 范围查询
范围查询是 ZSET 最强大的功能之一。可以按排名(索引)、分数或字典序进行查询。
按排名(索引)查询
排名(Rank)是指成员在有序集合中的位置,从 0 开始计数。默认是按分数从小到大排序。
ZRANGE key start stop [WITHSCORES]
- 作用:按排名(索引)范围获取成员。索引从 0 开始,
start
和stop
都是包含的。支持负数索引(-1 表示最后一个成员,-2 表示倒数第二个,以此类推)。 - 参数:
start
: 起始索引。stop
: 结束索引。WITHSCORES
: 可选参数,如果指定,返回值将同时包含成员和它的分数。
- 返回值:一个成员列表。如果指定了
WITHSCORES
,则返回一个包含[member, score, member, score, ...]
的扁平列表。 - 时间复杂度:O(log N + M),其中 N 是集合成员数,M 是请求范围内的成员数。
-
示例 (
redis-cli
):
```bash
# 获取排名 0 到 2 的成员 (分数最低的 3 个)
redis> ZRANGE leaderboard 0 2
1) "user:3"
2) "user:1"
3) "user:2"获取所有成员 (0 到 -1)
redis> ZRANGE leaderboard 0 -1
1) "user:3"
2) "user:1"
3) "user:2"
4) "user:4"
5) "user:5"获取排名 1 到 3 的成员及分数
redis> ZRANGE leaderboard 1 3 WITHSCORES
1) "user:1"
2) "1150"
3) "user:2"
4) "1600"
5) "user:4"
6) "1700"
* **示例 (Python `redis-py`)**:
python获取 Top 3 (分数最低的 3 个)
top3_lowest = r.zrange('leaderboard', 0, 2)
print(f"Top 3 lowest scores: {top3_lowest}")输出: Top 3 lowest scores: ['user:3', 'user:1', 'user:2']
获取所有成员及分数
all_with_scores = r.zrange('leaderboard', 0, -1, withscores=True)
print(f"All members with scores: {all_with_scores}")输出: All members with scores: [('user:3', 900.0), ('user:1', 1150.0), ('user:2', 1600.0), ('user:4', 1700.0), ('user:5', 1800.0)]
注意:redis-py 默认将 WITHSCORES 的结果解析为 (member, score) 元组的列表
```
ZREVRANGE key start stop [WITHSCORES]
- 作用:按排名逆序(从大到小)获取成员。索引规则与
ZRANGE
相同,但应用于逆序排名。 - 参数/返回值/时间复杂度:与
ZRANGE
类似。 -
示例 (
redis-cli
):
```bash
# 获取分数最高的 3 个成员 (逆序排名 0 到 2)
redis> ZREVRANGE leaderboard 0 2
1) "user:5"
2) "user:4"
3) "user:2"获取分数最高的 2 个成员及分数
redis> ZREVRANGE leaderboard 0 1 WITHSCORES
1) "user:5"
2) "1800"
3) "user:4"
4) "1700"
* **示例 (Python `redis-py`)**:
python获取 Top 3 (分数最高的 3 个)
top3_highest = r.zrevrange('leaderboard', 0, 2)
print(f"Top 3 highest scores: {top3_highest}")输出: Top 3 highest scores: ['user:5', 'user:4', 'user:2']
获取 Top 2 及分数
top2_with_scores = r.zrevrange('leaderboard', 0, 1, withscores=True)
print(f"Top 2 with scores: {top2_with_scores}")输出: Top 2 with scores: [('user:5', 1800.0), ('user:4', 1700.0)]
```
按分数范围查询
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
- 作用:获取有序集合中,分数在
min
和max
之间的所有成员(按分数从小到大排序)。 - 参数:
min
,max
: 分数范围的最小值和最大值。可以是-inf
(负无穷) 和+inf
(正无穷)。默认情况下,范围是包含边界的(即>= min
且<= max
)。可以在分数前加上(
来表示不包含边界(例如(1000
表示 > 1000)。WITHSCORES
: 可选,返回成员和分数。LIMIT offset count
: 可选,用于分页。在匹配范围内的结果中,跳过offset
个成员,然后返回最多count
个成员。
- 返回值:成员列表或
[member, score, ...]
列表(如果使用WITHSCORES
)。 - 时间复杂度:O(log N + M),N 为集合成员数,M 为匹配分数范围的成员数(在使用 LIMIT 前)。
-
示例 (
redis-cli
):
```bash
# 获取分数在 1000 到 1600 之间的成员 (包含边界)
redis> ZRANGEBYSCORE leaderboard 1000 1600
1) "user:1" # score 1150
2) "user:2" # score 1600获取分数大于 1000 且小于等于 1700 的成员,带分数
redis> ZRANGEBYSCORE leaderboard (1000 1700 WITHSCORES
1) "user:1"
2) "1150"
3) "user:2"
4) "1600"
5) "user:4"
6) "1700"获取分数大于等于 1000 的所有成员,带分数,取第 2、3 个 (offset 1, count 2)
redis> ZRANGEBYSCORE leaderboard 1000 +inf WITHSCORES LIMIT 1 2
1) "user:2"
2) "1600"
3) "user:4"
4) "1700"
* **示例 (Python `redis-py`)**:
python分数在 [1000, 1600]
range1 = r.zrangebyscore('leaderboard', 1000, 1600)
print(f"Score [1000, 1600]: {range1}") # 输出: Score [1000, 1600]: ['user:1', 'user:2']分数在 (1000, 1700], 带分数
range2 = r.zrangebyscore('leaderboard', '(1000', 1700, withscores=True)
print(f"Score (1000, 1700] with scores: {range2}")输出: Score (1000, 1700] with scores: [('user:1', 1150.0), ('user:2', 1600.0), ('user:4', 1700.0)]
分数 >= 1000, 带分数, 分页
range3 = r.zrangebyscore('leaderboard', 1000, '+inf', start=1, num=2, withscores=True)
print(f"Score >= 1000, page 2 (size 2): {range3}")输出: Score >= 1000, page 2 (size 2): [('user:2', 1600.0), ('user:4', 1700.0)]
```
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
- 作用:类似
ZRANGEBYSCORE
,但按分数从大到小的顺序返回结果。注意max
和min
参数的位置仍然是max min
,语义不变(即分数需>= min
且<= max
),只是返回顺序相反。 - 参数/返回值/时间复杂度:与
ZRANGEBYSCORE
类似。 -
示例 (
redis-cli
):
```bash
# 获取分数在 1000 到 1700 之间的成员,按分数从大到小排序
redis> ZREVRANGEBYSCORE leaderboard 1700 1000
1) "user:4"
2) "user:2"
3) "user:1"获取分数在 (1000, +inf) 之间的成员,带分数,逆序,取前 2 个
redis> ZREVRANGEBYSCORE leaderboard +inf (1000 WITHSCORES LIMIT 0 2
1) "user:5"
2) "1800"
3) "user:4"
4) "1700"
* **示例 (Python `redis-py`)**:
python分数在 [1000, 1700], 逆序
rev_range1 = r.zrevrangebyscore('leaderboard', 1700, 1000)
print(f"Score [1000, 1700] reversed: {rev_range1}")输出: Score [1000, 1700] reversed: ['user:4', 'user:2', 'user:1']
分数 > 1000, 带分数, 逆序, 取前 2 个
rev_range2 = r.zrevrangebyscore('leaderboard', '+inf', '(1000', start=0, num=2, withscores=True)
print(f"Score > 1000, reversed, top 2: {rev_range2}")输出: Score > 1000, reversed, top 2: [('user:5', 1800.0), ('user:4', 1700.0)]
```
按字典序范围查询 (Lexicographical)
这类命令要求所有成员必须具有相同的分数。常用于实现基于字符串前缀的自动补全等功能。
ZRANGEBYLEX key min max [LIMIT offset count]
- 作用:当 ZSET 中所有成员分数相同时,按成员的字典序返回指定范围内的成员。
- 参数:
min
,max
: 字典序范围。必须以[
或(
开头。[
表示包含边界,(
表示不包含边界。-
表示负无穷(字典序最小),+
表示正无穷(字典序最大)。LIMIT offset count
: 可选,用于分页。
- 返回值:成员列表。
- 时间复杂度:O(log N + M),N 为成员数,M 为匹配字典序范围的成员数(LIMIT 前)。
-
示例 (
redis-cli
):
```bash
# 准备一个所有成员分数相同的 ZSET
redis> ZADD myzset 0 a 0 b 0 c 0 d 0 e
(integer) 5获取字典序在 [b, d] 之间的成员
redis> ZRANGEBYLEX myzset [b [d
1) "b"
2) "c"
3) "d"获取字典序在 (a, c) 之间的成员
redis> ZRANGEBYLEX myzset (a (c
1) "b"获取以 'b' 开头的所有成员 (使用范围 [b, b\xff)
\xff 是 ASCII 码最大的字符,通常用来表示 "之后的所有字符"
redis> ZADD myzset 0 ba 0 bb 0 bc
(integer) 3
redis> ZRANGEBYLEX myzset [b (c
1) "b"
2) "ba"
3) "bb"
4) "bc"分页获取
redis> ZRANGEBYLEX myzset - + LIMIT 2 3
1) "c"
2) "d"
3) "e"
* **示例 (Python `redis-py`)**:
python
r.zadd('myzset', {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'ba': 0, 'bb': 0, 'bc': 0})Lex range [b, d]
lex_range1 = r.zrangebylex('myzset', '[b', '[d')
print(f"Lex range [b, d]: {lex_range1}") # 输出: Lex range [b, d]: ['b', 'ba', 'bb', 'bc', 'c', 'd'] (注意,是按字典序)Lex range (a, c)
lex_range2 = r.zrangebylex('myzset', '(a', '(c')
print(f"Lex range (a, c): {lex_range2}") # 输出: Lex range (a, c): ['b', 'ba', 'bb', 'bc']Lex range starting with 'b'
lex_range3 = r.zrangebylex('myzset', '[b', '(c') # [b, c) 包含了 b, ba, bb, bc
print(f"Lex range [b, c): {lex_range3}") # 输出: Lex range [b, c): ['b', 'ba', 'bb', 'bc']Lex range with limit
lex_range4 = r.zrangebylex('myzset', '-', '+', start=2, num=3)
print(f"Lex range all, page (offset 2, count 3): {lex_range4}") # 输出: Lex range all, page (offset 2, count 3): ['b', 'ba', 'bb']
```
ZREVRANGEBYLEX key max min [LIMIT offset count]
- 作用:类似
ZRANGEBYLEX
,但按字典序逆序返回成员。注意max
和min
参数的位置仍然是max min
。 - 参数/返回值/时间复杂度:与
ZRANGEBYLEX
类似。
ZLEXCOUNT key min max
- 作用:计算在字典序范围
[min, max]
内的成员数量。参数min
,max
的格式与ZRANGEBYLEX
相同。 - 返回值:整数,表示范围内的成员数量。
- 时间复杂度:O(log N),N 为成员总数。
- 示例 (
redis-cli
):
bash
redis> ZLEXCOUNT myzset [b [d
(integer) 6 # b, ba, bb, bc, c, d - 示例 (Python
redis-py
):
python
count = r.zlexcount('myzset', '[b', '[d')
print(f"Lex count [b, d]: {count}") # 输出: Lex count [b, d]: 6
4. 统计计算
ZCARD key
- 作用:获取有序集合
key
的成员数量(基数)。 - 返回值:整数,成员数量。如果 key 不存在,返回 0。
- 时间复杂度:O(1)
- 示例 (
redis-cli
):
bash
redis> ZCARD leaderboard
(integer) 5 - 示例 (Python
redis-py
):
python
cardinality = r.zcard('leaderboard')
print(f"Cardinality of leaderboard: {cardinality}") # 输出: Cardinality of leaderboard: 5
ZCOUNT key min max
- 作用:计算有序集合
key
中,分数在min
和max
范围内的成员数量。参数min
,max
的格式与ZRANGEBYSCORE
相同(支持-inf
,+inf
,(
)。 - 返回值:整数,分数在指定范围内的成员数量。
- 时间复杂度:O(log N),N 为成员总数。这比
ZRANGEBYSCORE
后再计算数量要快得多。 - 示例 (
redis-cli
):
bash
redis> ZCOUNT leaderboard 1000 1700
(integer) 3 # user:1 (1150), user:2 (1600), user:4 (1700)
redis> ZCOUNT leaderboard (1000 +inf
(integer) 4 # > 1000 的有 user:1, user:2, user:4, user:5 -
示例 (Python
redis-py
):
```python
count1 = r.zcount('leaderboard', 1000, 1700)
print(f"Count score [1000, 1700]: {count1}") # 输出: Count score [1000, 1700]: 3count2 = r.zcount('leaderboard', '(1000', '+inf')
print(f"Count score > 1000: {count2}") # 输出: Count score > 1000: 4
```
ZRANK key member
- 作用:获取成员
member
在有序集合key
中的排名(按分数从小到大排序,排名从 0 开始)。 - 返回值:整数形式的排名。如果成员不存在,返回
nil
。 - 时间复杂度:O(log N)
-
示例 (
redis-cli
):
```bash
redis> ZRANGE leaderboard 0 -1 WITHSCORES # 回顾一下当前的排序和分数
1) "user:3"
2) "900"
3) "user:1"
4) "1150"
5) "user:2"
6) "1600"
7) "user:4"
8) "1700"
9) "user:5"
10) "1800"redis> ZRANK leaderboard user:3
(integer) 0
redis> ZRANK leaderboard user:2
(integer) 2
redis> ZRANK leaderboard non_existent_user
(nil)
* **示例 (Python `redis-py`)**:
python
rank1 = r.zrank('leaderboard', 'user:3')
print(f"Rank of user:3: {rank1}") # 输出: Rank of user:3: 0rank2 = r.zrank('leaderboard', 'user:2')
print(f"Rank of user:2: {rank2}") # 输出: Rank of user:2: 2rank_non_exist = r.zrank('leaderboard', 'non_existent_user')
print(f"Rank of non_existent_user: {rank_non_exist}") # 输出: Rank of non_existent_user: None
```
ZREVRANK key member
- 作用:获取成员
member
在有序集合key
中的逆序排名(按分数从大到小排序,排名从 0 开始)。 - 返回值:整数形式的逆序排名。如果成员不存在,返回
nil
。 - 时间复杂度:O(log N)
- 示例 (
redis-cli
):
bash
redis> ZREVRANK leaderboard user:5 # 分数最高,逆序排名第 0
(integer) 0
redis> ZREVRANK leaderboard user:1 # 分数排第 2 低,逆序排第 3 (总共5个,排名 0, 1, 2, 3, 4)
(integer) 3 -
示例 (Python
redis-py
):
```python
rev_rank1 = r.zrevrank('leaderboard', 'user:5')
print(f"Reverse rank of user:5: {rev_rank1}") # 输出: Reverse rank of user:5: 0rev_rank2 = r.zrevrank('leaderboard', 'user:1')
print(f"Reverse rank of user:1: {rev_rank2}") # 输出: Reverse rank of user:1: 3
```
5. 删除成员
ZREM key member [member ...]
- 作用:从有序集合
key
中删除一个或多个指定的成员。忽略不存在的成员。 - 返回值:整数,成功删除的成员数量。
- 时间复杂度:O(M * log N),M 是要删除的成员数量,N 是集合总成员数。
- 示例 (
redis-cli
):
bash
redis> ZREM leaderboard user:3 user:non_existent
(integer) 1 # 成功删除了 user:3
redis> ZCARD leaderboard
(integer) 4 # 成员数减少了 1 - 示例 (Python
redis-py
):
python
removed_count = r.zrem('leaderboard', 'user:1', 'user:non_existent')
print(f"Removed members: {removed_count}") # 输出: Removed members: 1
print(f"New cardinality: {r.zcard('leaderboard')}") # 输出: New cardinality: 3 (假设之前有4个)
ZREMRANGEBYRANK key start stop
- 作用:删除有序集合
key
中,排名在start
和stop
范围内的所有成员(排名按分数从小到大,从 0 开始)。start
和stop
是包含的,支持负数索引。 - 返回值:整数,被删除的成员数量。
- 时间复杂度:O(log N + M),N 为成员总数,M 为被删除的成员数量。
- 示例 (
redis-cli
):
bash
# 假设当前 leaderboard: user:1(1150), user:2(1600), user:4(1700), user:5(1800)
# 排名 0: user:1, 排名 1: user:2, 排名 2: user:4, 排名 3: user:5
# 删除排名 0 和 1 的成员
redis> ZREMRANGEBYRANK leaderboard 0 1
(integer) 2
redis> ZRANGE leaderboard 0 -1
1) "user:4"
2) "user:5" - 示例 (Python
redis-py
):
python
# 先恢复一下数据
r.zadd('leaderboard', {'user:1': 1150, 'user:2': 1600, 'user:4': 1700, 'user:5': 1800})
removed_by_rank = r.zremrangebyrank('leaderboard', 0, 1)
print(f"Removed by rank 0-1: {removed_by_rank}") # 输出: Removed by rank 0-1: 2
print(f"Remaining members: {r.zrange('leaderboard', 0, -1)}") # 输出: Remaining members: ['user:4', 'user:5']
ZREMRANGEBYSCORE key min max
- 作用:删除有序集合
key
中,分数在min
和max
范围内的所有成员。min
,max
参数格式与ZRANGEBYSCORE
相同。 - 返回值:整数,被删除的成员数量。
- 时间复杂度:O(log N + M),N 为成员总数,M 为被删除的成员数量。
- 示例 (
redis-cli
):
bash
# 恢复数据
redis> ZADD leaderboard 1150 user:1 1600 user:2 1700 user:4 1800 user:5
(integer) 4
# 删除分数在 [1600, 1750] 之间的成员
redis> ZREMRANGEBYSCORE leaderboard 1600 1750
(integer) 2 # user:2 (1600), user:4 (1700) 被删除
redis> ZRANGE leaderboard 0 -1 WITHSCORES
1) "user:1"
2) "1150"
3) "user:5"
4) "1800" - 示例 (Python
redis-py
):
python
r.zadd('leaderboard', {'user:1': 1150, 'user:2': 1600, 'user:4': 1700, 'user:5': 1800})
removed_by_score = r.zremrangebyscore('leaderboard', 1600, 1750)
print(f"Removed by score [1600, 1750]: {removed_by_score}") # 输出: Removed by score [1600, 1750]: 2
print(f"Remaining members with scores: {r.zrange('leaderboard', 0, -1, withscores=True)}")
# 输出: Remaining members with scores: [('user:1', 1150.0), ('user:5', 1800.0)]
ZREMRANGEBYLEX key min max
- 作用:删除有序集合
key
中,成员字典序在min
和max
范围内的所有成员。要求集合中所有成员分数必须相同。min
,max
参数格式与ZRANGEBYLEX
相同。 - 返回值:整数,被删除的成员数量。
- 时间复杂度:O(log N + M),N 为成员总数,M 为被删除的成员数量。
6. 增量操作
ZINCRBY key increment member
- 作用:将有序集合
key
中成员member
的分数增加increment
(可以是负数,表示减少)。如果member
不存在,则添加它,初始分数为increment
。 - 返回值:成员的新分数(字符串形式)。
- 时间复杂度:O(log N)
- 示例 (
redis-cli
):
bash
redis> ZSCORE leaderboard user:1
"1150"
redis> ZINCRBY leaderboard 100 user:1
"1250"
redis> ZINCRBY leaderboard -50 user:1
"1200"
redis> ZINCRBY leaderboard 500 new_user
"500" # 添加了 new_user -
示例 (Python
redis-py
):
```python
# 恢复 user:1 的分数
r.zadd('leaderboard', {'user:1': 1150})
new_score1 = r.zincrby('leaderboard', 100, 'user:1')
print(f"New score for user:1: {new_score1}") # 输出: New score for user:1: 1250.0new_score2 = r.zincrby('leaderboard', -50, 'user:1')
print(f"New score for user:1 after decrease: {new_score2}") # 输出: New score for user:1 after decrease: 1200.0new_user_score = r.zincrby('leaderboard', 500, 'new_user')
print(f"Score for new_user: {new_user_score}") # 输出: Score for new_user: 500.0
```
7. 集合运算
ZSET 支持强大的交集和并集运算,并且可以对参与运算的集合设置权重,以及指定分数聚合方式。结果会存储在一个新的 ZSET 中。
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
- 作用:计算一个或多个有序集合的并集,并将结果存储在
destination
有序集合中。如果destination
已存在,会被覆盖。 - 参数:
destination
: 存储结果的目标 ZSET 键名。numkeys
: 要进行并集计算的源 ZSET 数量。key [key ...]
: 一个或多个源 ZSET 的键名。WEIGHTS weight [weight ...]
: 可选,为每个源 ZSET 指定一个权重因子(浮点数)。成员在结果集中的分数将是它在各源 ZSET 中分数乘以对应权重后的聚合值。默认权重为 1。权重列表长度必须等于numkeys
。AGGREGATE SUM|MIN|MAX
: 可选,指定如何聚合成员在不同源 ZSET 中的分数(乘以权重后)。默认是SUM
(求和)。MIN
取最小值,MAX
取最大值。
- 返回值:整数,结果集
destination
中的成员数量。 - 时间复杂度:O(N log N + M log M),其中 N 是所有输入 ZSET 的总成员数(最坏情况,无重复成员),M 是结果 ZSET 的成员数。复杂度较高,尤其是在大集合上。
-
示例 (
redis-cli
):
```bash
redis> ZADD zset1 1 one 2 two
(integer) 2
redis> ZADD zset2 1 one 3 three 4 four
(integer) 3简单并集 (默认权重 1, 聚合 SUM)
redis> ZUNIONSTORE out_union 2 zset1 zset2
(integer) 4 # one, two, three, four
redis> ZRANGE out_union 0 -1 WITHSCORES
1) "two"
2) "2"
3) "three"
4) "3"
5) "one" # one 在 zset1 分数 1, zset2 分数 1, SUM = 1+1 = 2 (?? 应该是笔误,redis-cli 这里显示了4? 确认一下,是1+1=2)
# 修正:上面ZADD zset2 1 one 应为 2 one 来测试SUM
redis> ZADD zset2 2 one 3 three 4 four # 重新设置 zset2
(integer) 3
redis> ZUNIONSTORE out_union 2 zset1 zset2
(integer) 4
redis> ZRANGE out_union 0 -1 WITHSCORES
1) "two" # 来自 zset1, score 2
2) "2"
3) "one" # 来自 zset1 (1) + zset2 (2) = 3
4) "3"
5) "three" # 来自 zset2, score 3
6) "3"
7) "four" # 来自 zset2, score 4
8) "4"带权重的并集 (zset1 权重 2, zset2 权重 3, 聚合 SUM)
redis> ZUNIONSTORE out_weighted_union 2 zset1 zset2 WEIGHTS 2 3
(integer) 4
redis> ZRANGE out_weighted_union 0 -1 WITHSCORES
1) "two" # score = 2 * 2 = 4
2) "4"
3) "one" # score = 1 * 2 + 2 * 3 = 8
4) "8"
5) "three" # score = 3 * 3 = 9
6) "9"
7) "four" # score = 4 * 3 = 12
8) "12"并集,聚合方式为 MAX (默认权重 1)
redis> ZUNIONSTORE out_max_union 2 zset1 zset2 AGGREGATE MAX
(integer) 4
redis> ZRANGE out_max_union 0 -1 WITHSCORES
1) "one" # score = MAX(1, 2) = 2
2) "2"
3) "two" # score = MAX(2, not_exist) = 2
4) "2"
5) "three" # score = MAX(not_exist, 3) = 3
6) "3"
7) "four" # score = MAX(not_exist, 4) = 4
8) "4"
* **示例 (Python `redis-py`)**:
python
r.zadd('zset1', {'one': 1, 'two': 2})
r.zadd('zset2', {'one': 2, 'three': 3, 'four': 4})简单并集
union_count = r.zunionstore('out_union', ['zset1', 'zset2'])
print(f"Union count: {union_count}") # 输出: Union count: 4
print(f"Union result: {r.zrange('out_union', 0, -1, withscores=True)}")输出: Union result: [('two', 2.0), ('one', 3.0), ('three', 3.0), ('four', 4.0)]
带权重并集
weighted_union_count = r.zunionstore('out_weighted_union', {'zset1': 2, 'zset2': 3}) # 使用字典传递权重
print(f"Weighted union count: {weighted_union_count}") # 输出: Weighted union count: 4
print(f"Weighted union result: {r.zrange('out_weighted_union', 0, -1, withscores=True)}")输出: Weighted union result: [('two', 4.0), ('one', 8.0), ('three', 9.0), ('four', 12.0)]
并集,聚合 MAX
max_union_count = r.zunionstore('out_max_union', ['zset1', 'zset2'], aggregate='MAX')
print(f"Max union count: {max_union_count}") # 输出: Max union count: 4
print(f"Max union result: {r.zrange('out_max_union', 0, -1, withscores=True)}")输出: Max union result: [('one', 2.0), ('two', 2.0), ('three', 3.0), ('four', 4.0)]
```
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
- 作用:计算一个或多个有序集合的交集,并将结果存储在
destination
有序集合中。只有同时存在于所有源 ZSET 中的成员才会被包含在结果中。 - 参数/返回值/时间复杂度:与
ZUNIONSTORE
类似,但计算的是交集。分数聚合逻辑相同,但只作用于交集中的成员。 - 时间复杂度:通常比
ZUNIONSTORE
快,因为它处理的成员数量可能更少。复杂度与输入集合大小和结果集大小有关。 -
示例 (
redis-cli
):
```bash
# 使用之前的 zset1 和 zset2
# zset1: {one: 1, two: 2}
# zset2: {one: 2, three: 3, four: 4}
# 交集只有 "one"简单交集 (默认权重 1, 聚合 SUM)
redis> ZINTERSTORE out_inter 2 zset1 zset2
(integer) 1 # 只有 "one" 在交集中
redis> ZRANGE out_inter 0 -1 WITHSCORES
1) "one" # score = 1 + 2 = 3
2) "3"带权重的交集 (zset1 权重 2, zset2 权重 3, 聚合 SUM)
redis> ZINTERSTORE out_weighted_inter 2 zset1 zset2 WEIGHTS 2 3
(integer) 1
redis> ZRANGE out_weighted_inter 0 -1 WITHSCORES
1) "one" # score = 1 * 2 + 2 * 3 = 8
2) "8"交集,聚合方式为 MAX (默认权重 1)
redis> ZINTERSTORE out_max_inter 2 zset1 zset2 AGGREGATE MAX
(integer) 1
redis> ZRANGE out_max_inter 0 -1 WITHSCORES
1) "one" # score = MAX(1, 2) = 2
2) "2"
* **示例 (Python `redis-py`)**:
python简单交集
inter_count = r.zinterstore('out_inter', ['zset1', 'zset2'])
print(f"Intersection count: {inter_count}") # 输出: Intersection count: 1
print(f"Intersection result: {r.zrange('out_inter', 0, -1, withscores=True)}")输出: Intersection result: [('one', 3.0)]
带权重交集
weighted_inter_count = r.zinterstore('out_weighted_inter', {'zset1': 2, 'zset2': 3})
print(f"Weighted intersection count: {weighted_inter_count}") # 输出: Weighted intersection count: 1
print(f"Weighted intersection result: {r.zrange('out_weighted_inter', 0, -1, withscores=True)}")输出: Weighted intersection result: [('one', 8.0)]
交集,聚合 MAX
max_inter_count = r.zinterstore('out_max_inter', ['zset1', 'zset2'], aggregate='MAX')
print(f"Max intersection count: {max_inter_count}") # 输出: Max intersection count: 1
print(f"Max intersection result: {r.zrange('out_max_inter', 0, -1, withscores=True)}")输出: Max intersection result: [('one', 2.0)]
```
8. 迭代与弹出
ZSCAN key cursor [MATCH pattern] [COUNT count]
- 作用:增量迭代有序集合
key
中的成员及其分数。用于遍历大型 ZSET 而不阻塞服务器。 - 参数:
key
: ZSET 键名。cursor
: 迭代游标,第一次调用时为 0。每次调用会返回下一次迭代的游标。当返回的游标为 0 时,表示迭代完成。MATCH pattern
: 可选,只返回匹配pattern
的成员(类似KEYS
命令的模式)。COUNT count
: 可选,提示 Redis 每次调用大致返回多少元素(不是精确保证)。
- 返回值:一个包含两个元素的列表:第一个是下一次迭代的
cursor
(字符串),第二个是本次迭代返回的成员和分数列表(扁平格式[member, score, member, score, ...]
)。 - 时间复杂度:每次调用 O(1) 平均,完成整个迭代需要 O(N)。
-
示例 (
redis-cli
):
```bash
redis> ZADD big_zset 1 a 2 b 3 c 4 d 5 e 6 f 7 g 8 h 9 i 10 j
(integer) 10第一次迭代
redis> ZSCAN big_zset 0 COUNT 5
1) "7" # 下一个 cursor
2) 1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"
7) "d"
8) "4"
9) "e"
10) "5" # 返回了 5 对第二次迭代,使用上一次返回的 cursor "7"
redis> ZSCAN big_zset 7 COUNT 5
1) "0" # 返回 0,表示迭代结束
2) 1) "f"
2) "6"
3) "g"
4) "7"
5) "h"
6) "8"
7) "i"
8) "9"
9) "j"
10) "10" # 返回了剩余的 5 对
* **示例 (Python `redis-py`)**:
python
r.zadd('big_zset', {chr(97+i): i+1 for i in range(10)}) # a:1, b:2, ..., j:10cursor = '0'
all_elements = []
while cursor != '0':
cursor, data = r.zscan('big_zset', cursor=cursor, count=5)
# redis-py 的 zscan 返回的是 (cursor, dict), dict 是 {member: score}
# 需要确认,文档显示返回 (cursor, list_of_tuples)
# 查证:zscan 返回 (cursor, list_of_tuples) 如果 decode_responses=True
# 查证:zscan_iter 更好用
print(f"Cursor: {cursor}, Data chunk: {data}")
all_elements.extend(data) # data 是 [(member, score), ...]或者使用迭代器
print("\nUsing zscan_iter:")
all_elements_iter = []
for member, score in r.zscan_iter('big_zset', count=5):
print(f" Got: {member} -> {score}")
all_elements_iter.append((member, score))print(f"\nAll elements from scan: {sorted(all_elements_iter)}")
输出类似: All elements from scan: [('a', 1.0), ('b', 2.0), ..., ('j', 10.0)]
```
ZPOPMIN key [count]
(Redis 5.0+)
- 作用:移除并返回有序集合
key
中分数最低的count
个成员。原子操作。 - 参数:
key
: ZSET 键名。count
: 可选,要弹出的成员数量,默认为 1。
- 返回值:一个包含
[member, score, member, score, ...]
的扁平列表,按分数从低到高排序。如果 key 不存在或为空,返回空列表。 - 时间复杂度:O(log N * K),N 是成员总数,K 是要弹出的数量。
- 示例 (
redis-cli
):
bash
redis> ZADD myqueue 10 task1 5 task2 20 task3
(integer) 3
redis> ZPOPMIN myqueue 1 # 弹出分数最低的 1 个
1) "task2"
2) "5"
redis> ZPOPMIN myqueue 2 # 弹出分数最低的 2 个
1) "task1" # score 10
2) "10"
3) "task3" # score 20
4) "20"
redis> ZCARD myqueue
(integer) 0 -
示例 (Python
redis-py
):
```python
r.zadd('myqueue', {'task1': 10, 'task2': 5, 'task3': 20})
popped1 = r.zpopmin('myqueue', count=1)
print(f"Popped min 1: {popped1}") # 输出: Popped min 1: [('task2', 5.0)]popped2 = r.zpopmin('myqueue', count=2)
print(f"Popped min 2: {popped2}") # 输出: Popped min 2: [('task1', 10.0), ('task3', 20.0)]注意:redis-py >= 3.2 返回 [(member, score), ...] 列表
```
ZPOPMAX key [count]
(Redis 5.0+)
- 作用:移除并返回有序集合
key
中分数最高的count
个成员。原子操作。 - 参数/返回值/时间复杂度:与
ZPOPMIN
类似,但操作的是分数最高的成员,返回结果按分数从高到低排序。 - 示例 (
redis-cli
):
bash
redis> ZADD myqueue 10 task1 5 task2 20 task3
(integer) 3
redis> ZPOPMAX myqueue 1 # 弹出分数最高的 1 个
1) "task3"
2) "20"
redis> ZPOPMAX myqueue 2 # 弹出分数最高的 2 个
1) "task1" # score 10
2) "10"
3) "task2" # score 5
4) "5" -
示例 (Python
redis-py
):
```python
r.zadd('myqueue', {'task1': 10, 'task2': 5, 'task3': 20})
popped1 = r.zpopmax('myqueue', count=1)
print(f"Popped max 1: {popped1}") # 输出: Popped max 1: [('task3', 20.0)]popped2 = r.zpopmax('myqueue', count=2)
print(f"Popped max 2: {popped2}") # 输出: Popped max 2: [('task1', 10.0), ('task2', 5.0)]
```
BZPOPMIN key [key ...] timeout
(Redis 5.0+)
- 作用:
ZPOPMIN
的阻塞版本。它会阻塞连接,直到在给定的一个或多个 ZSET 中有一个非空,然后从中弹出分数最低的成员。或者直到超时timeout
(秒,可以是 0 表示无限等待,浮点数支持微秒精度)。 - 返回值:一个三元列表
[key, member, score]
,表示从哪个 key 弹出了哪个 member 及其 score。如果超时,返回nil
。 - 时间复杂度:O(log N)。
- 用途:常用于实现可靠的优先级队列。
BZPOPMAX key [key ...] timeout
(Redis 5.0+)
- 作用:
ZPOPMAX
的阻塞版本。类似BZPOPMIN
,但弹出分数最高的成员。 -
返回值/时间复杂度:与
BZPOPMIN
类似。 -
示例 (阻塞命令在
redis-cli
中演示不直观,通常在客户端代码中使用):
```python
# 假设有两个优先级队列 q1, q2
r.zadd('q1', {'high_priority': 100})
# q2 为空阻塞等待 5 秒,从 q1 或 q2 弹出最低优先级的任务
result = r.bzpopmin(['q1', 'q2'], timeout=5)
if result:
key, member, score = result
print(f"Popped '{member}' with score {score} from key '{key}'")
# 输出: Popped 'high_priority' with score 100.0 from key 'q1'
else:
print("Timeout, no element popped.")再次调用,此时 q1, q2 都为空,会超时
result = r.bzpopmin(['q1', 'q2'], timeout=2)
if not result:
print("Timeout confirmed.") # 输出: Timeout confirmed.类似地使用 bzpopmax
r.zadd('q1', {'low_priority': 10})
r.zadd('q2', {'medium_priority': 50})
result = r.bzpopmax(['q1', 'q2'], timeout=5)
if result:
key, member, score = result
print(f"Popped max '{member}' with score {score} from key '{key}'")
# 输出: Popped max 'medium_priority' with score 50.0 from key 'q2' (因为 50 > 10)
```
ZSET 常见应用场景
- 排行榜 (Leaderboards):
ZADD
添加用户和分数。ZINCRBY
更新用户分数。ZREVRANGE
获取 Top N 用户列表。ZREVRANK
获取用户的排名。ZSCORE
获取用户的分数。
- 优先级队列 (Priority Queues):
- 用分数表示优先级(或时间戳)。分数越小(或越大)优先级越高。
ZADD
添加任务和优先级。BZPOPMIN
(或BZPOPMAX
) 原子地获取并移除优先级最高(或最低)的任务。
- 范围查询 (Range Queries):
- 例如,查找某个分数段内的用户、商品等。
ZRANGEBYSCORE
/ZREVRANGEBYSCORE
/ZCOUNT
。
- 时间轴 (Timelines):
- 用时间戳作为分数,消息 ID 或内容作为成员。
ZADD
添加新消息。ZRANGEBYSCORE
/ZREVRANGEBYSCORE
获取某个时间段内的消息。
- 带权重的自动补全 (Weighted Autocomplete):
- 如果所有可能补全项的分数不同(例如按热度),可以使用
ZREVRANGEBYSCORE
结合LIMIT
来获取最热门的 N 个匹配项。 - 如果需要纯字典序补全(所有项分数相同),使用
ZRANGEBYLEX
。
- 如果所有可能补全项的分数不同(例如按热度),可以使用
性能与内存考虑
- 时间复杂度:ZSET 的大部分操作(添加、删除、更新、排名、分数查询、范围查询)的时间复杂度都是 O(log N),其中 N 是集合大小。这使得 ZSET 即使在包含数百万成员时也能保持高效。集合运算 (
ZUNIONSTORE
,ZINTERSTORE
) 和批量删除 (ZREMRANGE*
) 的复杂度较高,需要注意。 - 内存占用:ZSET 同时使用哈希表和跳跃表,内存占用相对较高。每个成员需要存储 member 字符串、score (double, 8 字节),以及在哈希表和跳跃表中的指针开销。当成员数量较少且成员字符串较短时,Redis 可能使用 ziplist 编码来优化内存,但随着集合增大,会转换为更耗内存的 skip list + hash table 结构。
总结
Redis 的有序集合 (ZSET) 是一个功能极其强大的数据结构,它结合了集合的唯一性和列表的有序性(通过分数),并提供了丰富的命令集来进行添加、删除、查询、排序、范围检索、集合运算等操作。其 O(log N) 的核心操作效率使其非常适合构建排行榜、优先级队列、时间轴、范围查询等多种应用。熟练掌握 ZSET 的常用命令及其选项,对于高效利用 Redis 解决实际问题至关重要。希望本文的详细介绍和代码示例能为您在实践中运用 ZSET 提供有力的支持。