
🔥草莓熊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 命令)。本文讲的是集合数据结构类型。
两个核心特性
- 元素无序 这里的无序是相对于 List 的 "位置有序" 而言的:List 中元素调换顺序就是完全不同的列表;Set 没有位置概念,元素顺序不影响集合本身的同一性。比如
{1,2,3}和{2,1,3}是同一个集合。
注意:这里的无序是 "不保证遍历顺序",不是 "随机排序",和 ZSet 按分数排序的有序有本质区别,不要混淆。
- 元素唯一不重复 同一个元素在集合里只能存在一份,写入重复元素会被自动忽略。这是 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 编码:
- 集合中所有元素都是整数;
- 元素个数小于
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;
三个关键设计点:
- 有序存储 + 二分查找:元素按升序排列,查找用二分法,时间复杂度 O (logN)。虽然比哈希表的 O (1) 慢,但元素少的时候差异可以忽略。
- 编码自动升级:小整数用 int16 存储,加入大整数后自动整体升级为 int32、int64,保证数组内元素类型一致,同时尽可能节省空间。
- 连续内存的代价:插入删除需要移动后续元素,O (N) 开销。元素少时开销可接受,元素多了性能就不足以支撑,因此超过阈值后切换为哈希表。
这又是 Redis 一贯的设计哲学:小数据量优先省内存,大数据量优先保性能。极端的理论最优解往往工程成本极高,合适的折中才是生产环境的最佳选择。
集合运算的实现逻辑
交集运算的大致思路非常朴素高效:
- 先找出元素最少的集合作为遍历基准;
- 遍历小集合的每个元素,去其他集合中检查是否存在;
- 所有集合都存在的元素,加入结果集。
所以实际开销主要取决于最小集合的大小,这也是时间复杂度和最小集合强相关的原因。生产环境做交集运算时,尽量把小集合放在前面,能有效降低整体计算量。

四. 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% 精确。

核心考点总结
- 核心特性:元素无序、唯一不重复;注意和 List 的位置有序、ZSet 的分数排序做好区分。
- 核心命令:基础增删查、随机操作、集合运算的用法与返回值含义;store 版本命令的使用场景。
- 生产风险:大集合执行 smembers 会阻塞主线程;多集合交集运算开销较高,注意控制规模与执行时机。
- 底层编码:intset 与 hashtable 两种编码的适用条件、切换阈值;intset 有序数组 + 编码升级的设计思路。
- 应用场景:用户标签、共同好友与好友推荐、UV 统计,能说明实现思路和选用 Set 的原因。
- 设计思想:小数据量省空间、大数据量保性能的时空权衡;用服务端集合运算替代客户端循环,提升整体效率。
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:Set 的魅力不在于单个元素的增删改查,而在于原生的集合运算能力,能让我们用极简洁的方式解决标签、关系、去重这类典型业务问题。理解它的底层编码和适用边界,做技术选型时才能更有把握。下一篇我们会讲解 Redis 最有特色的数据结构 ------ZSet 有序集合,看看它在 Set 的基础上如何实现排序能力,以及跳表的底层实现原理。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
