2.5 有序集合 (Sorted Set)


文档摘要

2.5 有序集合 (Sorted Set) Redis 数据类型详解:2.5 有序集合 (Sorted Set) 的深度实践与应用 有序集合 (Sorted Set) 概述 有序集合 (Sorted Set) 是 Redis 提供的一种类似于集合的数据类型,它与集合 (Set) 的主要区别在于,有序集合中的每个成员都关联了一个分数 (Score),这个分数可以是浮点数。Redis 会根据分数对有序集合中的成员进行升序排序。 关键特性: 成员唯一性: 与集合 (Set) 类似,有序集合中的成员也是唯一的,不允许重复。 分数排序: 有序集合中的成员根据关联的分数进行排序,分数可以重复。

2.5 有序集合 (Sorted Set)

Redis 数据类型详解:2.5 有序集合 (Sorted Set) 的深度实践与应用

1. 有序集合 (Sorted Set) 概述

有序集合 (Sorted Set) 是 Redis 提供的一种类似于集合的数据类型,它与集合 (Set) 的主要区别在于,有序集合中的每个成员都关联了一个分数 (Score),这个分数可以是浮点数。Redis 会根据分数对有序集合中的成员进行升序排序

关键特性:

  • 成员唯一性: 与集合 (Set) 类似,有序集合中的成员也是唯一的,不允许重复。

  • 分数排序: 有序集合中的成员根据关联的分数进行排序,分数可以重复。

  • 快速查找: Redis 使用高效的数据结构(跳跃表和哈希表)来实现有序集合,使得添加、删除和查找成员的操作都非常快速,平均时间复杂度为 O(log(N)),其中 N 是有序集合的大小。

  • 范围查询: 有序集合支持根据分数范围或成员排名范围进行高效的查询操作。

适用场景:

有序集合由于其排序特性和高效的范围查询能力,在许多场景下都有广泛的应用,例如:

  • 排行榜/积分榜: 可以使用分数表示用户的积分,成员表示用户ID,轻松实现各种排行榜功能。

  • 优先级队列: 可以使用分数表示任务的优先级,成员表示任务内容,实现优先级队列。

  • 时间序列数据: 可以使用时间戳作为分数,数据点作为成员,方便按时间范围查询数据。

  • 索引系统: 可以使用关键词的权重作为分数,文档ID作为成员,构建基于权重的搜索索引。

  • 限流器: 可以使用时间戳作为分数,用户请求信息作为成员,实现基于滑动窗口的限流。

2. 有序集合的内部实现 (简述)

为了实现高效的排序和查找,Redis 有序集合底层使用了两种数据结构组合:

  • 跳跃表 (Skip List): 跳跃表是一种有序的数据结构,它允许快速的平均 O(log(N)) 时间复杂度的插入、删除和查找操作。跳跃表通过多层索引来加速查找,类似于一个多层的链表,每一层都是下一层链表的子集。Redis 使用跳跃表来维护有序集合中成员的排序。

  • 哈希表 (Hash Table): 哈希表用于存储成员到分数的映射关系,以及成员到跳跃表节点的指针。这使得可以通过成员快速查找其分数,以及在跳跃表中定位成员的位置,实现 O(1) 时间复杂度的成员查找。

这两种数据结构的结合,使得有序集合在保持高效排序的同时,也能快速进行成员的查找和访问。

3. 有序集合常用命令详解与代码实践 (Python redis-py)

以下将详细介绍 Redis 有序集合的常用命令,并结合 Python 的 redis-py 客户端库进行代码实践演示。

3.1 添加成员: ZADD

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

  • 作用: 向有序集合 key 中添加一个或多个成员 member,并为每个成员设置关联的 score

  • 选项:

    • NX: 只在成员不存在时添加。

    • XX: 只在成员已存在时更新分数。

    • CH: 修改返回值为发生变化的成员总数(新增和修改的成员)。默认返回值是新增成员的数量。

    • INCR: 将成员的分数增加 score,相当于 ZINCRBY 命令,但 ZADD INCR 只能用于单个成员。

  • 返回值: 新增成员的数量 (默认情况下,除非使用了 CH 选项)。

代码示例:

import redis # 连接 Redis (根据实际情况修改连接信息) r = redis.Redis(host='localhost', port=6379, db=0) # 清空测试数据 (可选) r.flushdb() # 添加单个成员 r.zadd('leaderboard', {'player1': 100}) print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player1', 100.0)] # 添加多个成员 r.zadd('leaderboard', {'player2': 150, 'player3': 80}) print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 80.0), (b'player1', 100.0), (b'player2', 150.0)] # 添加已存在的成员,更新分数 r.zadd('leaderboard', {'player1': 120}) # player1 的分数更新为 120 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 80.0), (b'player1', 120.0), (b'player2', 150.0)] # 使用 NX 选项,只添加不存在的成员 r.zadd('leaderboard', {'player4': 200}, nx=True) # player4 添加成功 r.zadd('leaderboard', {'player1': 300}, nx=True) # player1 已存在,添加失败,分数不变 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 80.0), (b'player1', 120.0), (b'player2', 150.0), (b'player4', 200.0)] # 使用 XX 选项,只更新已存在的成员分数 r.zadd('leaderboard', {'player5': 250}, xx=True) # player5 不存在,更新失败 r.zadd('leaderboard', {'player2': 180}, xx=True) # player2 存在,分数更新为 180 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 80.0), (b'player1', 120.0), (b'player2', 180.0), (b'player4', 200.0)] # 使用 CH 选项,返回修改的成员数量 count = r.zadd('leaderboard', {'player1': 120, 'player6': 300}, ch=True) # player1 分数未变,player6 新增,修改数量为 1 print(count) # 输出: 1 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 80.0), (b'player1', 120.0), (b'player2', 180.0), (b'player4', 200.0), (b'player6', 300.0)] # 使用 INCR 选项,增加成员分数 incremented_score = r.zadd('leaderboard', {'player3': 20}, incr=True) # player3 分数增加 20 print(incremented_score) # 输出: 100.0 (增加后的分数) print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player3', 100.0), (b'player1', 120.0), (b'player2', 180.0), (b'player4', 200.0), (b'player6', 300.0)]

3.2 获取成员数量: ZCARD

ZCARD key

  • 作用: 返回有序集合 key 的成员数量。

  • 返回值: 有序集合的成员数量。

代码示例:

cardinality = r.zcard('leaderboard') print(cardinality) # 输出: 5

3.3 获取指定范围的成员: ZRANGEZREVRANGE

ZRANGE key start stop [WITHSCORES]

ZREVRANGE key start stop [WITHSCORES]

  • 作用:

    • ZRANGE: 返回有序集合 key 中,指定排名范围 [start, stop] 的成员。成员按照分数升序排列。

    • ZREVRANGE: 返回有序集合 key 中,指定排名范围 [start, stop] 的成员。成员按照分数降序排列。

  • 参数:

    • start: 起始排名 (0 表示第一个成员,-1 表示最后一个成员)。

    • stop: 结束排名。

    • WITHSCORES: 可选选项,如果指定,则返回成员及其分数。

  • 返回值: 指定排名范围内的成员列表 (或成员和分数的列表,如果使用了 WITHSCORES 选项)。

代码示例:

# 获取排名 0 到 2 的成员 (升序) range_members = r.zrange('leaderboard', 0, 2) print(range_members) # 输出: [b'player3', b'player1', b'player2'] # 获取排名 0 到 2 的成员和分数 (升序) range_members_with_scores = r.zrange('leaderboard', 0, 2, withscores=True) print(range_members_with_scores) # 输出: [(b'player3', 100.0), (b'player1', 120.0), (b'player2', 180.0)] # 获取排名倒数 0 到 2 的成员 (降序) reverse_range_members = r.zrevrange('leaderboard', 0, 2) print(reverse_range_members) # 输出: [b'player6', b'player4', b'player2'] # 获取排名倒数 0 到 2 的成员和分数 (降序) reverse_range_members_with_scores = r.zrevrange('leaderboard', 0, 2, withscores=True) print(reverse_range_members_with_scores) # 输出: [(b'player6', 300.0), (b'player4', 200.0), (b'player2', 180.0)]

3.4 获取指定分数范围的成员: ZRANGEBYSCOREZREVRANGEBYSCORE

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

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

  • 作用:

    • ZRANGEBYSCORE: 返回有序集合 key 中,分数在 [min, max] 范围内的成员。成员按照分数升序排列。

    • ZREVRANGEBYSCORE: 返回有序集合 key 中,分数在 [min, max] 范围内的成员。成员按照分数降序排列。

  • 参数:

    • min: 最小分数 (可以使用 (min 表示不包含 min+inf 表示正无穷大,-inf 表示负无穷大)。

    • max: 最大分数 (可以使用 (max 表示不包含 max+inf 表示正无穷大,-inf 表示负无穷大)。

    • WITHSCORES: 可选选项,如果指定,则返回成员及其分数。

    • LIMIT offset count: 可选选项,用于分页,offset 表示起始偏移量,count 表示返回成员的数量。

  • 返回值: 指定分数范围内的成员列表 (或成员和分数的列表,如果使用了 WITHSCORES 选项)。

代码示例:

# 获取分数在 100 到 200 之间的成员 (升序,包含 100 和 200) score_range_members = r.zrangebyscore('leaderboard', 100, 200) print(score_range_members) # 输出: [b'player3', b'player1', b'player2', b'player4'] # 获取分数在 100 到 200 之间的成员和分数 (升序,不包含 200) score_range_members_with_scores = r.zrangebyscore('leaderboard', 100, '(200', withscores=True) print(score_range_members_with_scores) # 输出: [(b'player3', 100.0), (b'player1', 120.0), (b'player2', 180.0)] # 获取分数在 200 到 100 之间的成员 (降序,包含 200 和 100) reverse_score_range_members = r.zrevrangebyscore('leaderboard', 200, 100) print(reverse_score_range_members) # 输出: [b'player4', b'player2', b'player1', b'player3'] # 分页获取分数在 100 到 +inf 之间的成员 (升序,从偏移量 1 开始,返回 2 个成员) paged_score_range_members = r.zrangebyscore('leaderboard', 100, '+inf', offset=1, count=2) print(paged_score_range_members) # 输出: [b'player1', b'player2']

3.5 删除成员: ZREM

ZREM key member [member ...]

  • 作用: 从有序集合 key 中删除一个或多个指定的成员 member

  • 返回值: 成功删除的成员数量。

代码示例:

removed_count = r.zrem('leaderboard', 'player3', 'player5') # player3 存在,player5 不存在 print(removed_count) # 输出: 1 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player1', 120.0), (b'player2', 180.0), (b'player4', 200.0), (b'player6', 300.0)]

3.6 删除指定排名范围的成员: ZREMRANGEBYRANK

ZREMRANGEBYRANK key start stop

  • 作用: 删除有序集合 key 中,排名在 [start, stop] 范围内的成员。

  • 返回值: 成功删除的成员数量。

代码示例:

removed_count = r.zremrangebyrank('leaderboard', 0, 1) # 删除排名 0 和 1 的成员 (player1 和 player2) print(removed_count) # 输出: 2 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player4', 200.0), (b'player6', 300.0)]

3.7 删除指定分数范围的成员: ZREMRANGEBYSCORE

ZREMRANGEBYSCORE key min max

  • 作用: 删除有序集合 key 中,分数在 [min, max] 范围内的成员。

  • 返回值: 成功删除的成员数量。

代码示例:

removed_count = r.zremrangebyscore('leaderboard', 200, 250) # 删除分数在 200 到 250 之间的成员 (player4) print(removed_count) # 输出: 1 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'player6', 300.0)]

3.8 获取成员排名: ZRANKZREVRANK

ZRANK key member

ZREVRANK key member

  • 作用:

    • ZRANK: 返回有序集合 key 中成员 member 的排名 (升序排名,排名从 0 开始)。如果成员不存在,返回 None

    • ZREVRANK: 返回有序集合 key 中成员 member 的排名 (降序排名,排名从 0 开始)。如果成员不存在,返回 None

  • 返回值: 成员的排名 (整数),或 None (成员不存在)。

代码示例:

r.zadd('leaderboard', {'playerA': 50, 'playerB': 100, 'playerC': 150}) rank_asc = r.zrank('leaderboard', 'playerB') print(rank_asc) # 输出: 1 (升序排名,playerB 排在第 2 位,索引为 1) rank_desc = r.zrevrank('leaderboard', 'playerB') print(rank_desc) # 输出: 1 (降序排名,playerB 排在倒数第 2 位,索引为 1) rank_nonexistent = r.zrank('leaderboard', 'playerD') print(rank_nonexistent) # 输出: None

3.9 获取成员分数: ZSCORE

ZSCORE key member

  • 作用: 返回有序集合 key 中成员 member 的分数。如果成员不存在,返回 None

  • 返回值: 成员的分数 (浮点数),或 None (成员不存在)。

代码示例:

score = r.zscore('leaderboard', 'playerC') print(score) # 输出: 150.0 score_nonexistent = r.zscore('leaderboard', 'playerD') print(score_nonexistent) # 输出: None

3.10 增加成员分数: ZINCRBY

ZINCRBY key increment member

  • 作用: 将有序集合 key 中成员 member 的分数增加 increment。如果成员不存在,则会先添加成员,并将其初始分数设置为 increment

  • 返回值: 增加后的成员分数。

代码示例:

new_score = r.zincrby('leaderboard', 25, 'playerB') # playerB 的分数增加 25 print(new_score) # 输出: 125.0 new_score_nonexistent = r.zincrby('leaderboard', 10, 'playerD') # playerD 不存在,会被添加,初始分数为 10 print(new_score_nonexistent) # 输出: 10.0 print(r.zrange('leaderboard', 0, -1, withscores=True)) # 输出: [(b'playerA', 50.0), (b'playerD', 10.0), (b'playerB', 125.0), (b'playerC', 150.0)]

3.11 迭代有序集合: ZSCAN

ZSCAN key cursor [MATCH pattern] [COUNT count]

  • 作用: 用于迭代有序集合 key 中的成员,类似于 SCAN 命令,但只迭代有序集合。

  • 参数:

    • cursor: 游标,初始值为 0,每次迭代后返回新的游标值。当游标值为 0 时,表示迭代结束。

    • MATCH pattern: 可选选项,用于匹配成员名称的模式。

    • COUNT count: 可选选项,提示每次迭代返回的成员数量 (实际返回数量可能小于或等于 count)。

  • 返回值: 包含两个元素的元组:

    • 第一个元素是新的游标值。

    • 第二个元素是匹配到的成员列表 (每个成员是成员和分数的元组)。

代码示例:

r.zadd('users:scores', {'user1': 10, 'user2': 20, 'user3': 30, 'user4': 40, 'user5': 50}) cursor = '0' while cursor != '0': cursor, data = r.zscan('users:scores', cursor=cursor, count=2) print(f"Cursor: {cursor}, Data: {data}")

3.12 集合操作: ZINTERSTOREZUNIONSTORE

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

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

  • 作用:

    • ZINTERSTORE: 计算多个有序集合的交集,并将结果存储到新的有序集合 destination 中。

    • ZUNIONSTORE: 计算多个有序集合的并集,并将结果存储到新的有序集合 destination 中。

  • 参数:

    • destination: 目标有序集合的键名。

    • numkeys: 参与运算的有序集合的数量。

    • key [key ...]: 参与运算的有序集合的键名列表。

    • WEIGHTS weight [weight ...]: 可选选项,为每个输入有序集合设置权重,在聚合操作时使用。

    • AGGREGATE SUM|MIN|MAX: 可选选项,指定聚合函数,用于处理相同成员在不同有序集合中的分数:

      • SUM: 分数求和 (默认)。

      • MIN: 取最小分数。

      • MAX: 取最大分数。

  • 返回值: 结果有序集合的成员数量。

代码示例:

r.zadd('set1', {'a': 1, 'b': 2, 'c': 3}) r.zadd('set2', {'b': 4, 'c': 5, 'd': 6}) r.zadd('set3', {'c': 7, 'd': 8, 'e': 9}) # 交集,默认聚合函数 SUM r.zinterstore('intersection_set', ['set1', 'set2']) # 交集结果存储到 intersection_set print(r.zrange('intersection_set', 0, -1, withscores=True)) # 输出: [(b'c', 8.0), (b'b', 6.0)] (c: 3+5=8, b: 2+4=6) # 并集,聚合函数 MAX r.zunionstore('union_set', ['set1', 'set2', 'set3'], aggregate='MAX') # 并集结果存储到 union_set print(r.zrange('union_set', 0, -1, withscores=True)) # 输出: [(b'a', 1.0), (b'b', 4.0), (b'd', 8.0), (b'e', 9.0), (b'c', 7.0)] (取每个成员的最大分数) # 带权重的交集,聚合函数 SUM r.zinterstore('weighted_intersection_set', ['set1', 'set2'], weights=[2, 0.5], aggregate='SUM') # set1 权重 2, set2 权重 0.5 print(r.zrange('weighted_intersection_set', 0, -1, withscores=True)) # 输出: [(b'c', 9.5), (b'b', 6.0)] (c: 3*2 + 5*0.5 = 8.5, b: 2*2 + 4*0.5 = 6)

3.13 字典序操作: ZRANGEBYLEX, ZREVRANGEBYLEX, ZREMRANGEBYLEX, ZLEXCOUNT

这些命令用于在有序集合中根据成员的字典序进行范围查询和操作。注意:字典序操作要求有序集合中所有成员的分数都相同。 通常情况下,我们会将所有成员的分数设置为 0 或 1。

  • ZRANGEBYLEX key min max [LIMIT offset count]: 返回字典序在 [min, max] 范围内的成员 (升序)。

  • ZREVRANGEBYLEX key max min [LIMIT offset count]: 返回字典序在 [min, max] 范围内的成员 (降序)。

  • ZREMRANGEBYLEX key min max: 删除字典序在 [min, max] 范围内的成员。

  • ZLEXCOUNT key min max: 统计字典序在 [min, max] 范围内的成员数量。

字典序范围的表示:

  • [: 包含边界值。

  • (: 不包含边界值。

  • +: 正无穷大。

  • -: 负无穷大。

代码示例:

r.zadd('lex_set', { 'apple': 0, 'banana': 0, 'cherry': 0, 'date': 0, 'elderberry': 0, 'fig': 0 }) # 字典序范围查询 (升序,包含 'banana',不包含 'date') lex_range_members = r.zrangebylex('lex_set', '[banana', '(date') print(lex_range_members) # 输出: [b'banana', b'cherry'] # 字典序范围查询 (降序,从 'fig' 到 'apple',不包含 'apple') reverse_lex_range_members = r.zrevrangebylex('lex_set', '[fig', '(apple') print(reverse_lex_range_members) # 输出: [b'fig', b'elderberry', b'date', b'cherry', b'banana'] # 字典序范围删除 removed_lex_count = r.zremrangebylex('lex_set', '[cherry', '[date') # 删除 'cherry' 和 'date' print(removed_lex_count) # 输出: 2 print(r.zrangebylex('lex_set', '-', '+')) # 输出: [b'apple', b'banana', b'elderberry', b'fig'] # 字典序范围计数 lex_count = r.zlexcount('lex_set', '[banana', '[fig') # 统计 'banana' 到 'fig' (包含) 之间的成员数量 print(lex_count) # 输出: 3

4. 有序集合的应用场景深入

除了前面提到的基本应用场景,有序集合在更复杂的系统中也能发挥重要作用:

  • 实时分析与 Top N 统计: 可以使用有序集合存储实时数据流,以时间戳或某种指标作为分数,数据点作为成员。通过 ZREVRANGEBYSCOREZREVRANGE 命令,可以快速获取 Top N 的数据,例如实时热门商品、热门文章等。

  • 基于地理位置的服务 (LBS): 可以使用地理位置的经纬度信息计算距离作为分数,地点 ID 作为成员,构建地理位置索引。通过 ZRANGEBYSCORE 可以查询附近范围内的地点。Redis 提供了专门的地理位置命令 (GEO 命令),底层也是基于有序集合实现的。

  • 分布式锁的实现 (基于分数和时间戳): 可以使用有序集合来实现分布式锁,将锁的名称作为键,请求锁的时间戳作为分数,客户端标识作为成员。通过 ZADD NX 命令尝试获取锁,通过 ZREMRANGEBYSCORE 命令释放超时锁。

  • 会话管理 (Session Management): 可以使用用户会话的最后活跃时间作为分数,会话 ID 作为成员,实现会话超时管理和清理。通过 ZREMRANGEBYSCORE 可以定期清理过期的会话。

  • API 访问频率控制 (Rate Limiting): 可以使用时间戳作为分数,请求标识作为成员,实现基于滑动窗口的限流。通过 ZREMRANGEBYSCORE 删除窗口外的请求,通过 ZCARD 获取窗口内的请求数量,判断是否超过限制。

5. 有序集合的性能考量

  • 时间复杂度: 有序集合的大部分操作 (添加、删除、查找、范围查询) 的平均时间复杂度为 O(log(N)),其中 N 是有序集合的大小。ZCARDZSCORE 等命令的时间复杂度为 O(1)。这使得有序集合在处理大量数据时仍然能保持较高的性能。

  • 内存使用: 有序集合使用跳跃表和哈希表两种数据结构,相比于简单的集合或列表,内存占用会相对较高。但是,Redis 的高效内存管理机制和数据压缩技术可以在一定程度上缓解内存压力。

  • 性能优化建议:

    • 控制有序集合的大小: 避免单个有序集合过大,可以考虑分片或使用更细粒度的键。

    • 合理选择命令: 根据实际需求选择合适的命令,例如范围查询时优先使用 ZRANGEBYSCOREZRANGE,而不是遍历整个有序集合。

    • 监控性能指标: 监控 Redis 的性能指标,例如内存使用、CPU 负载等,及时发现和解决性能瓶颈。

6. 总结

有序集合 (Sorted Set) 是 Redis 中一种功能强大且应用广泛的数据类型。它在集合的基础上引入了分数的概念,实现了成员的排序和高效的范围查询。本文详细介绍了有序集合的特性、内部实现、常用命令、代码实践以及应用场景,并对性能进行了简要分析。

掌握有序集合的使用,能够帮助开发者更有效地利用 Redis 解决各种实际问题,构建高性能、可扩展的应用系统。在实际应用中,应根据具体场景选择合适的操作命令,并注意性能优化,充分发挥有序集合的优势。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U