【Redis 初阶】Set 类型深度解析:去重集合的运算能力与实战场景


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言
  • [一. Set 类型基本介绍](#一. Set 类型基本介绍)
  • [二. Set 核心命令全解](#二. Set 核心命令全解)
    • [2.1 基础增删查:sadd /smembers/sismember /scard](#2.1 基础增删查:sadd /smembers/sismember /scard)
    • [2.2 随机操作:spop /srandmember](#2.2 随机操作:spop /srandmember)
    • [2.3 移动与删除:smove /srem](#2.3 移动与删除:smove /srem)
    • [2.4 集合运算:交、并、差三大操作](#2.4 集合运算:交、并、差三大操作)
    • [2.5 命令小结](#2.5 命令小结)
  • [三. Set 底层编码实现](#三. Set 底层编码实现)
    • [3.1 intset 整数集合](#3.1 intset 整数集合)
    • [3.2 hashtable 哈希表](#3.2 hashtable 哈希表)
  • [四. Set 典型应用场景](#四. Set 典型应用场景)
    • [4.1 用户标签系统](#4.1 用户标签系统)
    • [4.2 共同好友与好友推荐](#4.2 共同好友与好友推荐)
    • [4.3 UV 统计](#4.3 UV 统计)
  • 结尾:

前言

前面我们依次拆解了 String、Hash、List 三种核心类型,今天我们来看 Set 集合类型。很多开发者对 Set 的印象只停留在 "自动去重",但它真正的核心价值是原生支持集合交、并、差运算,在标签系统、好友关系、UV 统计这类场景里,比客户端自己写循环去重要高效得多。本文从基础特性讲起,逐个拆解 Set 的核心命令与生产踩坑点,深入 intset 和 hashtable 两种底层编码的设计逻辑,再结合三个经典业务场景讲透实战用法,带你从 "会用命令" 理解到 "为什么这么设计"。


一. Set 类型基本介绍

先澄清一个术语歧义:Set 在编程语境里有两个常见含义,一是 "集合" 数据结构,二是 "设置" 类操作(比如 set 命令)。本文讲的是集合数据结构类型。

两个核心特性

  1. 元素无序 这里的无序是相对于 List 的 "位置有序" 而言的:List 中元素调换顺序就是完全不同的列表;Set 没有位置概念,元素顺序不影响集合本身的同一性。比如 {1,2,3}{2,1,3} 是同一个集合。

注意:这里的无序是 "不保证遍历顺序",不是 "随机排序",和 ZSet 按分数排序的有序有本质区别,不要混淆。

  1. 元素唯一不重复 同一个元素在集合里只能存在一份,写入重复元素会被自动忽略。这是 Set 最核心的特性,也是绝大多数场景选用它的根本原因。

Set 中的每个元素也都是字符串类型,同样支持存储 JSON 等结构化字符串数据。


二. Set 核心命令全解

Set 相关命令都以 S 为前缀,辨识度很高,我们按功能分组逐一讲解。

2.1 基础增删查:sadd /smembers/sismember /scard

sadd:添加元素

bash 复制代码
SADD key member [member ...]

向集合中添加一个或多个元素,重复元素会被自动忽略。返回值是本次操作成功新增的元素个数。 时间复杂度:O (k),k 为添加的元素个数。

示例:

bash 复制代码
127.0.0.1:6379> sadd tag:1 java redis mysql
(integer) 3
# 重复添加 java,返回 0
127.0.0.1:6379> sadd tag:1 java
(integer) 0

smembers:获取所有元素

bash 复制代码
SMEMBERS key

返回集合中全部元素,结果顺序不固定。 时间复杂度:O (N),N 为集合元素总数。

⚠️ 生产环境红线:和 keys *hgetall 同理,大集合执行 smembers 会全量遍历,长时间阻塞 Redis 主线程。如果需要遍历大集合,应使用 sscan 渐进式遍历,化整为零避免卡顿。

sismember:判断元素是否存在

bash 复制代码
SISMEMBER key member

判断指定元素是否在集合中,存在返回 1,不存在返回 0。 时间复杂度:O(1),这是哈希表底层结构带来的核心优势,也是去重判断效率极高的原因。

scard:获取元素总数

bash 复制代码
SCARD key

返回集合的元素总个数,时间复杂度 O (1)。底层有专门的计数器记录长度,直接读取即可,不需要遍历。 这个命令非常实用,比如统计 UV 总数时,直接用 scard 就能拿到结果。

2.2 随机操作:spop /srandmember

spop:随机弹出元素

bash 复制代码
SPOP key [count]

随机删除并返回集合中的一个或多个元素,count 参数指定弹出数量,默认弹出一个。 时间复杂度:O (n),n 为弹出的元素个数。

因为集合没有头尾的概念,所以 pop 操作是完全随机的。官方文档明确承诺随机行为,源码内部通过生成随机索引来实现。 非常适合抽奖、随机选题这类场景。

srandmember:随机获取元素

bash 复制代码
SRANDMEMBER key [count]

随机返回集合中的元素,但只读取不删除。count 可以指定返回个数。 和 spop 的区别就是一个移除元素、一个保留元素,适合随机推荐、随机展示这类不需要移除数据的场景。

2.3 移动与删除:smove /srem

smove:移动元素

bash 复制代码
SMOVE source destination member

原子性地将指定元素从源集合移动到目标集合。移动成功返回 1,源集合中不存在该元素则返回 0。 时间复杂度:O (1)。

整个操作是原子的,不会出现 "源集合删了、目标集合没加上" 的中间状态,在数据迁移、状态变更场景中非常实用。即使目标集合已经存在该元素,也会正常执行,不会报错。

srem:删除元素

bash 复制代码
SREM key member [member ...]

删除集合中一个或多个元素,返回值是成功删除的元素个数。 时间复杂度:O (k),k 为删除的元素个数。

小提示:Redis 不同命令的返回值规则差异很大,不必死记硬背,用的时候查官方文档最稳妥,常用命令自然会记住。

2.4 集合运算:交、并、差三大操作

这是 Set 最有价值的一组能力,也是它区别于其他数据结构的核心功能。

先明确三个基础概念:

  • 交集:同时出现在所有集合中的元素。例如 A={1,2,3,4},B={3,4,5,6},交集为 {3,4}
  • 并集:所有集合的全部元素汇总,重复元素只保留一份。上面例子的并集为 {1,2,3,4,5,6}
  • 差集:在第一个集合存在、但在后续集合中不存在的元素。注意差集不满足交换律:A-B 和 B-A 结果完全不同。上面例子中 A-B={1,2},B-A={5,6}

每组运算都有两个版本:直接返回结果的版本,和将结果存入新集合的 store 版本。

交集运算:sinter /sinterstore

bash 复制代码
# 直接返回交集结果
SINTER key [key ...]
# 将交集结果存入目标集合,返回交集元素个数
SINTERSTORE destination key [key ...]

时间复杂度:O (N*M),N 是最小集合的元素个数,M 是最大集合的元素个数。

示例:

bash 复制代码
127.0.0.1:6379> sadd set1 1 2 3 4
127.0.0.1:6379> sadd set2 3 4 5 6
127.0.0.1:6379> sinter set1 set2
1) "3"
2) "4"

# 把交集结果存到新集合 set3
127.0.0.1:6379> sinterstore set3 set1 set2
(integer) 2

生产提醒:多个大集合做交集运算开销较大,可能阻塞主线程。尽量控制集合规模,或者放在业务低峰期执行。

并集运算:sunion /sunionstore

bash 复制代码
# 直接返回并集结果
SUNION key [key ...]
# 将并集结果存入目标集合
SUNIONSTORE destination key [key ...]

时间复杂度:O (N),N 为所有集合的元素总数。

差集运算:sdiff /sdiffstore

bash 复制代码
# 直接返回差集结果
SDIFF key [key ...]
# 将差集结果存入目标集合
SDIFFSTORE destination key [key ...]

时间复杂度:O (N),N 为所有集合的元素总数。

store 系列命令非常实用,当运算结果需要后续继续使用时,直接在服务端存成新集合,省去客户端接收再回写的网络开销。

2.5 命令小结

命令 作用 时间复杂度
sadd key member member ... 添加元素 O (k),k 为添加个数
srem key member member ... 删除元素 O (k),k 为删除个数
scard key 获取元素总数 O(1)
sismember key member 判断元素是否存在 O(1)
srandmember key count 随机获取元素 O (n),n 为返回个数
spop key count 随机弹出元素 O (n),n 为弹出个数
smembers key 获取所有元素 O (k),k 为元素总数
sinter / sinterstore 交集运算 O(N*M)
sunion / sunionstore 并集运算 O (k),总元素数
sdiff / sdiffstore 差集运算 O (k),总元素数

三. Set 底层编码实现

Set 类型有两种底层编码,会根据数据情况自动切换:intset(整数集合)hashtable(哈希表)

3.1 intset 整数集合

当集合同时满足两个条件时,使用 intset 编码:

  1. 集合中所有元素都是整数;
  2. 元素个数小于 set-max-intset-entries 配置,默认值为 512。

intset 是专门为整数集合设计的紧凑结构,核心目标是节省内存。相比于哈希表,它省去了指针、哈希桶这些额外开销,用连续内存存储整数,空间利用率非常高。

3.2 hashtable 哈希表

只要不满足 intset 的任意一个条件(出现非整数元素、或元素数量超过阈值),就会自动切换为 hashtable 编码。 底层和 Hash 类型的哈希表实现一致,元素作为 key 存入哈希表,value 为空,利用哈希表的 O (1) 查找能力实现去重和快速存在性判断。

做个横向对比,避免和其他语言混淆:

  • C++ 的 std::set 底层是红黑树,有序,查找 O (logN);
  • Java 的 Set 是接口,常见实现有 HashSet(哈希表,无序)和 TreeSet(红黑树,有序);
  • Redis 的 Set 底层是 intset + 哈希表,无序,查找 O (1)。 Redis 中按分数排序的有序集合是 ZSet,和 Set 是完全不同的两种数据结构。

可以通过 OBJECT encoding 验证编码切换:

bash 复制代码
# 全是整数、数量少,使用 intset
127.0.0.1:6379> sadd numset 1 2 3 4
(integer) 4
127.0.0.1:6379> OBJECT encoding numset
"intset"

# 加入字符串,触发编码转换
127.0.0.1:6379> sadd numset hello
(integer) 1
127.0.0.1:6379> OBJECT encoding numset
"hashtable"

源码视角:intset 的空间优化智慧

站在 C 开发的视角看,intset 是非常经典的 "用少量性能换大量空间" 的工程优化案例。

intset 的核心结构

intset 本质是一个有序的整数数组,大致结构如下:

c 复制代码
typedef struct intset {
    uint32_t encoding;  // 编码类型:int16 / int32 / int64
    uint32_t length;    // 元素总个数
    int8_t contents[];  // 柔性数组,存储实际整数元素
} intset;

三个关键设计点:

  1. 有序存储 + 二分查找:元素按升序排列,查找用二分法,时间复杂度 O (logN)。虽然比哈希表的 O (1) 慢,但元素少的时候差异可以忽略。
  2. 编码自动升级:小整数用 int16 存储,加入大整数后自动整体升级为 int32、int64,保证数组内元素类型一致,同时尽可能节省空间。
  3. 连续内存的代价:插入删除需要移动后续元素,O (N) 开销。元素少时开销可接受,元素多了性能就不足以支撑,因此超过阈值后切换为哈希表。

这又是 Redis 一贯的设计哲学:小数据量优先省内存,大数据量优先保性能。极端的理论最优解往往工程成本极高,合适的折中才是生产环境的最佳选择。

集合运算的实现逻辑

交集运算的大致思路非常朴素高效:

  1. 先找出元素最少的集合作为遍历基准;
  2. 遍历小集合的每个元素,去其他集合中检查是否存在;
  3. 所有集合都存在的元素,加入结果集。

所以实际开销主要取决于最小集合的大小,这也是时间复杂度和最小集合强相关的原因。生产环境做交集运算时,尽量把小集合放在前面,能有效降低整体计算量。


四. Set 典型应用场景

4.1 用户标签系统

这是 Set 最经典的应用场景,也是用户画像、千人千面推荐的基础。 每个用户的标签存入一个 Set,天然自动去重,添加删除都很方便。例如:

Plain 复制代码
user:tag:1001 -> {java, redis, 后端, 篮球}

基于标签可以延伸出很多玩法:

  • sinter 计算两个用户的共同标签,做用户相似度匹配;
  • sunion 合并多个用户的标签,做群体画像;
  • 反向也可以给每个标签建一个用户集合,比如 tag:java -> {1001, 1002, ...},方便按标签圈选目标用户。

当然,标签系统深入做下去会有专门的用户画像团队,而信息茧房也是这类系统带来的副作用 ------ 算法总推送你偏好的内容,视野会逐渐被限制在小圈子里。技术本身是中性的,如何使用才是关键。

4.2 共同好友与好友推荐

社交产品的好友关系非常适合用 Set 存储。每个用户的好友列表存入一个 Set:

Plain 复制代码
user:friends:1001 -> {1002, 1003, 1005}
  • 共同好友 :用 sinter 计算两个用户好友集合的交集,直接得到共同好友列表;
  • 好友推荐:基于差集运算,找出好友的好友中、你尚未添加的人,作为 "可能认识的人" 推荐给你。

这也是 QQ、微信同类功能的基础实现思路之一。

4.3 UV 统计

利用 Set 的自动去重特性,可以非常方便地统计页面或产品的独立访客数(UV)。 先明确两个指标的区别:

  • PV(页面浏览量) :每次访问都计数,重复访问累加,用 incr 即可实现;
  • UV(独立访客数):同一个用户多次访问只算一次,需要按用户维度去重。

实现方式:将访问用户的 userId(或 IP)通过 sadd 写入对应页面的 Set 中,自动去重。需要查看 UV 数值时,直接 scard 就能拿到结果。

bash 复制代码
# 用户 1001 访问首页
sadd page:home:uv 1001
# 查询首页 UV 总数
scard page:home:uv

补充说明:如果是超大规模 UV(百万、千万级),用 Set 存储全量用户 ID 会比较耗内存。这种场景更适合用 HyperLogLog 做概率统计,牺牲极少量精度换取极大的空间节省。但中小规模的 UV 统计,Set 完全够用,且结果 100% 精确。

核心考点总结

  1. 核心特性:元素无序、唯一不重复;注意和 List 的位置有序、ZSet 的分数排序做好区分。
  2. 核心命令:基础增删查、随机操作、集合运算的用法与返回值含义;store 版本命令的使用场景。
  3. 生产风险:大集合执行 smembers 会阻塞主线程;多集合交集运算开销较高,注意控制规模与执行时机。
  4. 底层编码:intset 与 hashtable 两种编码的适用条件、切换阈值;intset 有序数组 + 编码升级的设计思路。
  5. 应用场景:用户标签、共同好友与好友推荐、UV 统计,能说明实现思路和选用 Set 的原因。
  6. 设计思想:小数据量省空间、大数据量保性能的时空权衡;用服务端集合运算替代客户端循环,提升整体效率。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:Set 的魅力不在于单个元素的增删改查,而在于原生的集合运算能力,能让我们用极简洁的方式解决标签、关系、去重这类典型业务问题。理解它的底层编码和适用边界,做技术选型时才能更有把握。下一篇我们会讲解 Redis 最有特色的数据结构 ------ZSet 有序集合,看看它在 Set 的基础上如何实现排序能力,以及跳表的底层实现原理。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
张文君25 分钟前
ubuntu26.04坏道坏块分区隔离急速版260831-V0.12
linux·python
XR12345678827 分钟前
汽车制造园区网络怎么建?柔性产线与 AGV 的选型逻辑
网络·汽车·制造
煎饼皮皮侠2 小时前
【设计】设计一个web版的数据库管理平台后端(之五) --借鉴mybatis
数据库·mybatis
东风破_8 小时前
danci 2:创建的单词书到底存在哪里?从 Supabase 一路理解 ORM、Drizzle 和 RLS
数据库·后端·node.js
贝锐9 小时前
从国产化信创设备到商用安卓终端,向日葵如何帮助企业实现统一运维管理?
linux·运维·远程控制
橙子家9 小时前
OSS 文件上传的几个风险点和解决方案
数据库
IT大白鼠10 小时前
MSF二次开发与自定义模块编写
网络·安全·web安全·msf
Julien200410 小时前
调查和解决 SELinux 问题
linux·运维·服务器·网络·学习方法
2601_9620664911 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库