2.3 集合 (Set)


文档摘要

2.3 集合 (Set) 2.3 集合 (Set) 详解与实践 2.3.1 集合 (Set) 的核心特性 在深入代码实践之前,我们先来明确 Redis 集合的关键特性,这有助于我们理解其设计目的和适用场景: 唯一性 (Uniqueness): 这是集合最核心的特性。集合中不允许出现重复的成员。无论您尝试添加多少次相同的元素,集合中始终只保留一个副本。这种特性非常适合用于去重场景。 无序性 (Unordered): 集合中的元素是无序排列的。您无法像列表 (List) 那样通过索引来访问集合中的元素。Redis 内部使用哈希表来存储集合,这保证了元素唯一性的同时,也带来了无序性。

2.3 集合 (Set)

2.3 集合 (Set) 详解与实践

2.3.1 集合 (Set) 的核心特性

在深入代码实践之前,我们先来明确 Redis 集合的关键特性,这有助于我们理解其设计目的和适用场景:

  • 唯一性 (Uniqueness): 这是集合最核心的特性。集合中不允许出现重复的成员。无论您尝试添加多少次相同的元素,集合中始终只保留一个副本。这种特性非常适合用于去重场景。

  • 无序性 (Unordered): 集合中的元素是无序排列的。您无法像列表 (List) 那样通过索引来访问集合中的元素。Redis 内部使用哈希表来存储集合,这保证了元素唯一性的同时,也带来了无序性。

  • 成员关系高效检查 (Efficient Membership Testing): 由于底层使用了哈希表,Redis 可以非常快速地判断一个元素是否存在于集合中。这种高效的成员关系检查使得集合在需要快速查找和判断元素是否存在的场景中非常有用。

  • 集合操作 (Set Operations): Redis 集合支持丰富的集合操作,例如并集 (UNION)、交集 (INTERSECTION)、差集 (DIFFERENCE) 等。这些操作允许您对多个集合进行组合和分析,从而实现更复杂的数据处理逻辑。

2.3.2 常用命令与代码实践 (Python + redis-py)

为了更好地理解和应用 Redis 集合,我们将通过 Python 语言和流行的 Redis 客户端库 redis-py 来进行代码实践。以下是一些常用的 Redis 集合命令及其对应的 Python 代码示例和详细解释。

环境准备:

首先,确保您已经安装了 Redis 服务,并且安装了 redis-py 库。您可以使用 pip 进行安装:

pip install redis

在 Python 代码中,我们需要先连接到 Redis 服务器:

import redis # 连接 Redis (根据您的 Redis 配置修改) r = redis.Redis(host='localhost', port=6379, db=0) # 测试连接 try: r.ping() print("成功连接到 Redis!") except redis.exceptions.ConnectionError as e: print(f"连接 Redis 失败: {e}") exit()

1. 添加成员 (SADD):

SADD key member [member ...] 命令用于向集合 key 中添加一个或多个成员。如果成员已存在,则会被忽略。返回成功添加到集合的新成员的数量。

# 添加单个成员 added_count = r.sadd('my_set', 'apple') print(f"添加 'apple' 到集合,新增成员数: {added_count}") # 输出: 添加 'apple' 到集合,新增成员数: 1 # 添加多个成员 added_count = r.sadd('my_set', 'banana', 'orange', 'apple') # 'apple' 已存在,不会重复添加 print(f"添加 'banana', 'orange', 'apple' 到集合,新增成员数: {added_count}") # 输出: 添加 'banana', 'orange', 'apple' 到集合,新增成员数: 2 # 查看集合成员 (稍后介绍 SMEMBERS) members = r.smembers('my_set') print(f"集合 'my_set' 的成员: {members}") # 输出: 集合 'my_set' 的成员: {b'orange', b'banana', b'apple'} (注意:返回的是字节串)

代码解释:

  • r.sadd('my_set', 'apple'): 向名为 my_set 的集合中添加字符串 'apple'

  • 返回值 added_count: 表示成功添加到集合中的新成员数量。因为 'apple' 是首次添加,所以返回 1。

  • r.sadd('my_set', 'banana', 'orange', 'apple'): 尝试添加 'banana', 'orange', 'apple'。由于 'apple' 已经存在,所以实际上只添加了 'banana' 和 'orange',新增成员数为 2。

  • r.smembers('my_set'): 用于获取集合的所有成员 (稍后详细介绍)。

2. 获取集合成员数量 (SCARD):

SCARD key 命令用于获取集合 key 的成员数量 (cardinality)。如果 key 不存在,则返回 0。

cardinality = r.scard('my_set') print(f"集合 'my_set' 的成员数量: {cardinality}") # 输出: 集合 'my_set' 的成员数量: 3 cardinality_non_existent = r.scard('non_existent_set') print(f"不存在的集合 'non_existent_set' 的成员数量: {cardinality_non_existent}") # 输出: 不存在的集合 'non_existent_set' 的成员数量: 0

代码解释:

  • r.scard('my_set'): 获取集合 my_set 的成员数量,当前集合包含 'apple', 'banana', 'orange',所以返回 3。

  • r.scard('non_existent_set'): 尝试获取一个不存在的集合的成员数量,Redis 会将其视为空集合,返回 0。

3. 获取所有成员 (SMEMBERS):

SMEMBERS key 命令返回集合 key 中的所有成员。由于集合是无序的,返回的成员顺序是不确定的。

members = r.smembers('my_set') print(f"集合 'my_set' 的所有成员: {members}") # 输出: 集合 'my_set' 的所有成员: {b'orange', b'banana', b'apple'} (顺序可能不同) members_non_existent = r.smembers('non_existent_set') print(f"不存在的集合 'non_existent_set' 的所有成员: {members_non_existent}") # 输出: 不存在的集合 'non_existent_set' 的所有成员: set() (空集合)

代码解释:

  • r.smembers('my_set'): 获取集合 my_set 的所有成员,返回一个 Python 集合 (set) 对象。注意,Redis 返回的是字节串 (bytes),redis-py 会自动将其转换为 Python 的字节串类型。如果您需要字符串类型,需要进行解码 (decode)。

  • r.smembers('non_existent_set'): 对于不存在的集合,SMEMBERS 返回一个空集合。

4. 检查成员是否存在 (SISMEMBER):

SISMEMBER key member 命令用于检查成员 member 是否是集合 key 的成员。如果成员存在,返回 1,否则返回 0。

is_apple_member = r.sismember('my_set', 'apple') print(f"'apple' 是否是 'my_set' 的成员: {is_apple_member}") # 输出: 'apple' 是否是 'my_set' 的成员: True (或者 1) is_grape_member = r.sismember('my_set', 'grape') print(f"'grape' 是否是 'my_set' 的成员: {is_grape_member}") # 输出: 'grape' 是否是 'my_set' 的成员: False (或者 0) is_member_non_existent_set = r.sismember('non_existent_set', 'apple') print(f"'apple' 是否是 'non_existent_set' 的成员: {is_member_non_existent_set}") # 输出: 'apple' 是否是 'non_existent_set' 的成员: False (或者 0)

代码解释:

  • r.sismember('my_set', 'apple'): 检查 'apple' 是否是 my_set 的成员,返回 True (或 1,在 Redis 协议中实际返回整数 1)。

  • r.sismember('my_set', 'grape'): 检查 'grape' 是否是 my_set 的成员,返回 False (或 0)。

  • r.sismember('non_existent_set', 'apple'): 对于不存在的集合,任何成员检查都会返回 False (或 0)。

5. 移除成员 (SREM):

SREM key member [member ...] 命令用于从集合 key 中移除一个或多个成员。返回成功移除的成员数量 (不包括不存在的成员)。

removed_count = r.srem('my_set', 'banana') print(f"从 'my_set' 移除 'banana',移除成员数: {removed_count}") # 输出: 从 'my_set' 移除 'banana',移除成员数: 1 removed_count = r.srem('my_set', 'grape', 'orange') # 'grape' 不存在,'orange' 存在 print(f"从 'my_set' 移除 'grape', 'orange',移除成员数: {removed_count}") # 输出: 从 'my_set' 移除 'grape', 'orange',移除成员数: 1 (只移除了 'orange') members = r.smembers('my_set') print(f"集合 'my_set' 的成员: {members}") # 输出: 集合 'my_set' 的成员: {b'apple'}

代码解释:

  • r.srem('my_set', 'banana'): 从 my_set 移除 'banana',成功移除,返回 1。

  • r.srem('my_set', 'grape', 'orange'): 尝试移除 'grape' 和 'orange'。由于 'grape' 不存在,只有 'orange' 被成功移除,返回 1。

  • 再次使用 SMEMBERS 查看集合,可以看到 'banana' 和 'orange' 已经被移除,只剩下 'apple'。

6. 随机返回集合中的一个或多个成员 (SRANDMEMBER & SPOP):

  • SRANDMEMBER key [count]: 随机返回集合 key 中的一个或多个成员。如果指定 count,且为正数,则返回包含 count不同成员的列表。如果 count 为负数,则返回包含 |count| 个成员的列表,但允许重复成员。 如果 count 不指定或为 1,则返回单个随机成员。

  • SPOP key [count]: 随机移除并返回集合 key 中的一个或多个成员。如果指定 count,则随机移除并返回 count 个成员。 如果 count 不指定或为 1,则随机移除并返回单个成员。 注意:SPOP 会修改集合本身,移除返回的成员。

# SRANDMEMBER 示例 random_member = r.srandmember('my_set') # 返回单个随机成员 print(f"随机成员 (SRANDMEMBER): {random_member}") # 输出: 随机成员 (SRANDMEMBER): b'apple' (或者其他成员,如果集合有多个) random_members_2 = r.srandmember('my_set', 2) # 返回 2 个不同的随机成员列表 print(f"2 个随机成员 (SRANDMEMBER count=2): {random_members_2}") # 输出: 2 个随机成员 (SRANDMEMBER count=2): [b'apple'] (如果集合只有 1 个成员,则返回包含 1 个成员的列表) r.sadd('my_set', 'banana', 'orange', 'grape') # 先添加一些成员,以便演示 count > 集合大小的情况 random_members_5 = r.srandmember('my_set', 5) # count 大于集合大小,最多返回集合所有成员 print(f"5 个随机成员 (SRANDMEMBER count=5): {random_members_5}") # 输出: 5 个随机成员 (SRANDMEMBER count=5): [b'orange', b'grape', b'apple', b'banana'] (顺序可能不同) random_members_minus_3 = r.srandmember('my_set', -3) # count 为负数,允许重复 print(f"-3 个随机成员 (SRANDMEMBER count=-3): {random_members_minus_3}") # 输出: -3 个随机成员 (SRANDMEMBER count=-3): [b'banana', b'banana', b'apple'] (可能包含重复成员) # SPOP 示例 popped_member = r.spop('my_set') # 随机移除并返回一个成员 print(f"随机移除并返回成员 (SPOP): {popped_member}") # 输出: 随机移除并返回成员 (SPOP): b'grape' (或者其他随机成员) popped_members_2 = r.spop('my_set', 2) # 随机移除并返回 2 个成员 print(f"随机移除并返回 2 个成员 (SPOP count=2): {popped_members_2}") # 输出: 随机移除并返回 2 个成员 (SPOP count=2): [b'banana', b'orange'] (或者其他随机 2 个成员) members = r.smembers('my_set') # 查看集合,确认成员已被移除 print(f"集合 'my_set' 的成员 (SPOP 后): {members}") # 输出: 集合 'my_set' 的成员 (SPOP 后): {b'apple'} (或者空集合,取决于之前的 SPOP 操作)

代码解释:

  • SRANDMEMBER: 用于从集合中随机获取成员,但不修改集合本身。 可以指定返回多少个成员,以及是否允许重复。

  • SPOP: 用于从集合中随机移除并返回成员,会修改集合。 同样可以指定移除并返回多少个成员。

  • SRANDMEMBER 适合用于随机抽样、获取随机元素等场景,而 SPOP 适合用于需要从集合中随机取出元素并处理的场景 (例如,从待处理任务集合中随机取出一个任务)。

7. 集合运算 (SINTER, SUNION, SDIFF, SINTERSTORE, SUNIONSTORE, SDIFFSTORE):

Redis 集合支持强大的集合运算,允许您对多个集合进行操作:

  • SINTER key [key ...] (交集): 返回所有给定集合的交集,即所有集合都包含的成员。

  • SUNION key [key ...] (并集): 返回所有给定集合的并集,即所有集合包含的所有成员,去重后的结果。

  • SDIFF key [key ...] (差集): 返回集合 key 与其他所有给定集合的差集。差集是指在 key 中存在,但不在其他任何给定集合中存在的成员。

  • SINTERSTORE destination key [key ...] (交集并存储): 计算给定集合的交集,并将结果存储到 destination 集合中。

  • SUNIONSTORE destination key [key ...] (并集并存储): 计算给定集合的并集,并将结果存储到 destination 集合中。

  • SDIFFSTORE destination key [key ...] (差集并存储): 计算给定集合的差集,并将结果存储到 destination 集合中。

# 创建多个集合用于演示集合运算 r.sadd('set1', 'apple', 'banana', 'orange') r.sadd('set2', 'banana', 'grape', 'watermelon') r.sadd('set3', 'orange', 'grape', 'kiwi') # SINTER (交集) intersection_set = r.sinter('set1', 'set2') print(f"集合 'set1' 和 'set2' 的交集: {intersection_set}") # 输出: 集合 'set1' 和 'set2' 的交集: {b'banana'} intersection_sets_3 = r.sinter('set1', 'set2', 'set3') print(f"集合 'set1', 'set2', 'set3' 的交集: {intersection_sets_3}") # 输出: 集合 'set1', 'set2', 'set3' 的交集: set() (空集,因为没有共同成员) # SUNION (并集) union_set = r.sunion('set1', 'set2') print(f"集合 'set1' 和 'set2' 的并集: {union_set}") # 输出: 集合 'set1' 和 'set2' 的并集: {b'watermelon', b'orange', b'banana', b'apple', b'grape'} union_sets_3 = r.sunion('set1', 'set2', 'set3') print(f"集合 'set1', 'set2', 'set3' 的并集: {union_sets_3}") # 输出: 集合 'set1', 'set2', 'set3' 的并集: {b'watermelon', b'orange', b'banana', b'apple', b'grape', b'kiwi'} # SDIFF (差集) diff_set_1_2 = r.sdiff('set1', 'set2') # set1 相对于 set2 的差集 print(f"集合 'set1' 相对于 'set2' 的差集: {diff_set_1_2}") # 输出: 集合 'set1' 相对于 'set2' 的差集: {b'orange', b'apple'} (set1 中有,但 set2 中没有的) diff_set_2_1 = r.sdiff('set2', 'set1') # set2 相对于 set1 的差集 print(f"集合 'set2' 相对于 'set1' 的差集: {diff_set_2_1}") # 输出: 集合 'set2' 相对于 'set1' 的差集: {b'watermelon', b'grape'} (set2 中有,但 set1 中没有的) # SINTERSTORE, SUNIONSTORE, SDIFFSTORE (存储结果) r.sinterstore('intersection_result', 'set1', 'set2') # 交集结果存储到 'intersection_result' intersection_result_set = r.smembers('intersection_result') print(f"交集结果集合 'intersection_result': {intersection_result_set}") # 输出: 交集结果集合 'intersection_result': {b'banana'} r.sunionstore('union_result', 'set1', 'set2') # 并集结果存储到 'union_result' union_result_set = r.smembers('union_result') print(f"并集结果集合 'union_result': {union_result_set}") # 输出: 并集结果集合 'union_result': {b'watermelon', b'orange', b'banana', b'apple', b'grape'} r.sdiffstore('diff_result', 'set1', 'set2') # 差集结果存储到 'diff_result' diff_result_set = r.smembers('diff_result') print(f"差集结果集合 'diff_result': {diff_result_set}") # 输出: 差集结果集合 'diff_result': {b'orange', b'apple'}

代码解释:

  • SINTER, SUNION, SDIFF: 直接返回集合运算的结果,但不存储结果

  • SINTERSTORE, SUNIONSTORE, SDIFFSTORE: 将集合运算的结果存储到一个新的集合中 (第一个参数 destination)。 这在需要后续使用集合运算结果时非常有用。

  • 集合运算在数据分析、用户关系分析、权限控制等场景中非常有用。例如,可以使用交集来查找共同好友,使用并集来合并用户兴趣标签,使用差集来找出特定用户群体的差异。

8. 移动成员 (SMOVE):

SMOVE source destination member 命令将成员 membersource 集合移动到 destination 集合。如果 membersource 集合中存在,则将其移除并添加到 destination 集合。操作是原子性的。如果 source 集合不存在或不包含 member,则 SMOVE 不执行任何操作并返回 0。 如果 destination 集合不存在,则会自动创建。

r.sadd('source_set', 'item1', 'item2', 'item3') r.sadd('destination_set', 'item4', 'item5') moved_count = r.smove('source_set', 'destination_set', 'item2') print(f"移动 'item2' 从 'source_set' 到 'destination_set',移动结果: {moved_count}") # 输出: 移动 'item2' 从 'source_set' 到 'destination_set',移动结果: 1 source_members = r.smembers('source_set') print(f"移动后 'source_set' 的成员: {source_members}") # 输出: 移动后 'source_set' 的成员: {b'item1', b'item3'} ('item2' 已移除) destination_members = r.smembers('destination_set') print(f"移动后 'destination_set' 的成员: {destination_members}") # 输出: 移动后 'destination_set' 的成员: {b'item5', b'item4', b'item2'} ('item2' 已添加) moved_count_non_existent = r.smove('source_set', 'destination_set', 'non_existent_item') # 移动不存在的成员 print(f"移动 'non_existent_item',移动结果: {moved_count_non_existent}") # 输出: 移动 'non_existent_item',移动结果: 0 (未移动)

代码解释:

  • r.smove('source_set', 'destination_set', 'item2'): 将 'item2' 从 source_set 移动到 destination_set。 成功移动返回 1。

  • r.smove('source_set', 'destination_set', 'non_existent_item'): 尝试移动不存在的成员,操作无效,返回 0。

  • SMOVE 命令常用于任务队列、数据迁移等场景,确保数据在不同集合之间原子性地转移。

9. 迭代集合 (SSCAN):

对于大型集合,使用 SMEMBERS 一次性获取所有成员可能会导致性能问题 (阻塞 Redis 服务器或客户端内存溢出)。 SSCAN 命令提供了游标 (cursor) 迭代的方式,可以逐步遍历集合中的成员,避免一次性加载大量数据。

# 向集合中添加大量成员 (假设有 1000 个) for i in range(1000): r.sadd('large_set', f'member_{i}') cursor = '0' # 初始游标 count = 100 # 每次迭代获取的成员数量 while cursor != '0': cursor, members = r.sscan('large_set', cursor=cursor, count=count) print(f"游标: {cursor}, 获取成员数: {len(members)}, 成员示例: {members[:5]}...") # 打印部分成员 # 在这里处理获取到的成员 if cursor == '0': break # 游标为 0 时表示迭代完成 print("集合迭代完成!")

代码解释:

  • r.sscan('large_set', cursor=cursor, count=count): 使用 SSCAN 命令迭代集合。

    • cursor: 当前迭代的游标,初始为 '0'。

    • count: 每次迭代尝试获取的成员数量 (hint,Redis 不保证每次返回 exactly count 个成员,但会尽量接近)。

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

    • 新的游标值 (用于下次迭代)。 当游标为 '0' 时,表示迭代完成。

    • 获取到的成员列表。

  • 循环迭代,直到游标变为 '0'。

  • SSCAN 适用于遍历大型集合,避免阻塞 Redis 服务器和客户端。 与 SCAN 命令类似,但 SSCAN 专门用于集合。

2.3.3 集合 (Set) 的应用场景

Redis 集合由于其独特的特性,在许多场景下都能发挥重要作用:

  1. 标签 (Tags): 可以使用集合来存储与对象 (例如,文章、用户) 相关的标签。每个标签可以作为一个集合,对象 ID 作为集合的成员。通过集合的交集、并集等操作,可以方便地进行标签的组合查询和筛选。

    • 例如,文章标签集合: tags:article:{article_id} (成员为标签名称)

    • 用户兴趣标签集合: tags:user:{user_id} (成员为标签名称)

    • 查找同时拥有 "技术" 和 "Redis" 标签的用户: SINTER tags:user:tech tags:user:redis

  2. 社交关系 (Social Relationships): 可以使用集合来表示用户之间的关注关系、好友关系等。

    • 用户关注的人集合: following:{user_id} (成员为被关注用户的 ID)

    • 用户的粉丝集合: followers:{user_id} (成员为粉丝用户的 ID)

    • 共同关注: SINTER following:{user_id1} following:{user_id2}

    • 共同粉丝: SINTER followers:{user_id1} followers:{user_id2}

  3. 唯一数据跟踪 (Unique Data Tracking): 集合可以用于记录唯一事件或数据,例如网站的 UV (Unique Visitors)、用户的唯一行为等。

    • 每日 UV 统计: uv:date:{yyyyMMdd} (成员为用户 ID)

    • 统计某日 UV: SCARD uv:date:{yyyyMMdd}

  4. 投票系统 (Voting Systems): 可以使用集合来记录投票结果,确保每个用户只能投票一次。

    • 投票集合: votes:{item_id} (成员为投票用户的 ID)

    • 统计投票数: SCARD votes:{item_id}

    • 检查用户是否已投票: SISMEMBER votes:{item_id} {user_id}

  5. 权限控制 (Access Control Lists, ACLs): 可以使用集合来存储允许访问资源的用户的 ID 列表。

    • 资源访问权限集合: acl:{resource_id} (成员为允许访问用户的 ID)

    • 检查用户是否有访问权限: SISMEMBER acl:{resource_id} {user_id}

  6. 推荐系统 (Recommendation Systems): 可以使用集合来存储用户的兴趣偏好、物品的关联关系等,用于生成推荐列表。

    • 用户喜欢的物品集合: likes:{user_id} (成员为物品 ID)

    • 物品的关联用户集合: liked_by:{item_id} (成员为喜欢该物品的用户 ID)

    • 基于用户共同喜欢的物品进行推荐。

2.3.4 总结与最佳实践

Redis 集合是一种功能强大且灵活的数据类型,尤其擅长处理需要唯一性、无序性和高效成员关系检查的场景。 掌握其常用命令和集合运算,可以帮助您构建更高效、更简洁的 Redis 应用。

最佳实践建议:

  • 合理选择数据类型: 根据实际业务需求选择合适的数据类型。 如果需要存储有序元素,列表 (List) 可能更合适。如果需要键值对存储,哈希 (Hash) 或有序集合 (Sorted Set) 可能更合适。

  • 控制集合大小: 虽然 Redis 集合可以存储大量元素,但过大的集合会影响性能和内存消耗。 根据实际情况,考虑是否需要对集合进行分片或定期清理。

  • 谨慎使用 SMEMBERS: 对于大型集合,避免使用 SMEMBERS 一次性获取所有成员,使用 SSCAN 进行迭代遍历。

  • 利用集合运算: 充分利用 Redis 提供的集合运算命令,简化数据处理逻辑,提高效率。


发布者: 作者: 转发
评论区 (0)
U