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 的几个核心概念:

  1. 成员 (Member):集合中的元素,必须是唯一的字符串。
  2. 分数 (Score):与每个成员关联的浮点数(double-precision floating-point number)。Redis 使用分数对成员进行排序。分数可以重复。
  3. 排序 (Ordering):成员默认按分数从小到大排序。如果分数相同,则按成员的字典序(lexicographical order)排序(具体取决于 Redis 版本和配置,但通常是这样)。
  4. 唯一性 (Uniqueness):成员在 ZSET 中是唯一的,但分数可以相同。
  5. 内部实现:ZSET 的底层实现通常结合了跳跃表(Skip List)哈希表(Hash Table)。哈希表用于 O(1) 时间复杂度查找成员对应的分数,跳跃表则保证了在 O(log N) 时间复杂度内进行范围查询、排序等操作(N 为集合中的成员数量)。

ZSET 常用命令详解

我们将 ZSET 命令大致分为以下几类进行讲解:

  1. 添加与更新成员
  2. 查询成员与分数
  3. 范围查询(按排名、按分数、按字典序)
  4. 统计计算
  5. 删除成员
  6. 增量操作
  7. 集合运算(交集、并集)
  8. 迭代与弹出

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 redis

    r = 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 开始,startstop 都是包含的。支持负数索引(-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]

  • 作用:获取有序集合中,分数在 minmax 之间的所有成员(按分数从小到大排序)。
  • 参数
    • 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,但按分数从大到小的顺序返回结果。注意 maxmin 参数的位置仍然是 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,但按字典序逆序返回成员。注意 maxmin 参数的位置仍然是 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 中,分数在 minmax 范围内的成员数量。参数 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]: 3

    count2 = 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: 0

    rank2 = r.zrank('leaderboard', 'user:2')
    print(f"Rank of user:2: {rank2}") # 输出: Rank of user:2: 2

    rank_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: 0

    rev_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 中,排名在 startstop 范围内的所有成员(排名按分数从小到大,从 0 开始)。startstop 是包含的,支持负数索引。
  • 返回值:整数,被删除的成员数量。
  • 时间复杂度: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 中,分数在 minmax 范围内的所有成员。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 中,成员字典序在 minmax 范围内的所有成员。要求集合中所有成员分数必须相同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.0

    new_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.0

    new_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:10

    cursor = '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 常见应用场景

  1. 排行榜 (Leaderboards)
    • ZADD 添加用户和分数。
    • ZINCRBY 更新用户分数。
    • ZREVRANGE 获取 Top N 用户列表。
    • ZREVRANK 获取用户的排名。
    • ZSCORE 获取用户的分数。
  2. 优先级队列 (Priority Queues)
    • 用分数表示优先级(或时间戳)。分数越小(或越大)优先级越高。
    • ZADD 添加任务和优先级。
    • BZPOPMIN (或 BZPOPMAX) 原子地获取并移除优先级最高(或最低)的任务。
  3. 范围查询 (Range Queries)
    • 例如,查找某个分数段内的用户、商品等。
    • ZRANGEBYSCORE / ZREVRANGEBYSCORE / ZCOUNT
  4. 时间轴 (Timelines)
    • 用时间戳作为分数,消息 ID 或内容作为成员。
    • ZADD 添加新消息。
    • ZRANGEBYSCORE / ZREVRANGEBYSCORE 获取某个时间段内的消息。
  5. 带权重的自动补全 (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 提供有力的支持。


THE END