文章目录
- 第一章 集合Set
- 1 集合的核心概念与特性
- 2 集合内元素的常规操作
- 2.1 添加元素:SADD
- 2.2 获取全部元素:SMEMBERS
- 2.3 判断元素是否存在:SISMEMBER
- 2.4 获取集合基数:SCARD
- 2.5 随机移除元素:SPOP
- 2.6 移动元素:SMOVE
- 2.7 删除指定元素:SREM
- 3 集合间的运算操作
- 3.1 交集运算:SINTER 与 SINTERSTORE
- 3.2 并集运算:SUNION 与 SUNIONSTORE
- 3.3 差集运算:SDIFF 与 SDIFFSTORE
- 第二章 深入探究 Redis 集合的内部编码与底层实现
- 1 节省内存的利器:intset(整数集合)
- 2 通用且灵活的实现:hashtable(哈希表)
- 第三章 集合的典型应用场景与架构实践
- 1 核心应用:构建海量用户画像与标签系统
- 1.1 从用户特征到标签化存储
- 1.2 标签系统的双向索引与操作
- 1.3 标签衍生出的“用户关系”与信息茧房
- 2 社交网络:基于集合交集的好友推荐
- 3 流量统计:利用天然去重特性计算 UV
第一章 集合Set
在 Redis 的数据结构家族中,集合(Set)扮演着至关重要的角色。当业务场景涉及到数据去重、关系运算(如共同好友、共同兴趣)时,集合类型往往是绝佳的选择。本章将系统性地剖析 Redis 集合的核心特性、常用命令、底层编码以及其在实际工程中的应用。
1 集合的核心概念与特性
在数学中,集合是一组互不相同的对象的整体。Redis 的集合类型完美继承了这一数学概念,它同样用于保存多个字符串类型的元素,但与列表(List)有着本质的区别。Redis 中的集合主要有两大核心特性:
无序性:集合中的元素是没有顺序的。此处所说的“无序”与列表中的“有序”是相对应的。对于列表而言,顺序非常重要,变换一下顺序就是不同的列表,例如[1, 2, 3]和[2, 1, 3]是两个不同的列表。而对于集合而言,顺序不重要,变化一下顺序,集合还是那个集合,{1, 2, 3}和{2, 1, 3}是同一个集合。唯一性:集合中的元素是不能重复的。
- 1.
底层存储的实质是字符串:虽然我们感觉在往集合里放各种数据,但 Redis 集合的底层和列表一样,每个元素本质上都是string(字符串)。这意味着,如果你想往集合里存一个复杂的对象(比如用户的完整信息),你需要先把它序列化成 JSON 格式的字符串再存进去。Redis 本身并不理解对象结构,它只负责存字符串。 - 2. 容量非常庞大:一个集合最多可以容纳 (2^{32}-1) 个元素(大约是 42 亿个)。作为一个内存数据库,这个容量上限对于绝大多数实际业务场景来说,几乎是无限的,完全不用担心存满。
- 3. 强大的多集合运算能力:它不仅能做最基础的单集合操作(比如往集合里加人、踢人、查人数),它最强大的杀手锏是原生支持多个集合之间取交集、并集、差集。这意味着你可以把复杂的“关系计算”直接交给 Redis 来做。
2 集合内元素的常规操作
围绕单个集合内的元素,Redis 提供了一系列基础命令,涵盖了添加、查询、删除等操作。
2.1 添加元素:SADD
SADD命令用于将一个或者多个元素添加到 set 中。需要注意的是,由于集合的唯一性,重复的元素无法添加到 set 中,这很像 hash 类型中设置 field 的操作。
- 语法:
SADD key member [member ...] - 时间复杂度:
O(1) - 返回值:本次添加成功的元素个数。
示例:
redis> SADD myset "Hello" (integer) 1 redis> SADD myset "World" (integer) 1 redis> SADD myset "World" (integer) 02.2 获取全部元素:SMEMBERS
SMEMBERS命令用于获取一个 set 中的所有元素。由于集合是无序的,因此返回元素的顺序也是无序的。
- 语法:
SMEMBERS key - 时间复杂度:
O(N) - 返回值:所有元素的列表。
示例:
redis> SMEMBERS myset 1) "Hello" 2) "World"2.3 判断元素是否存在:SISMEMBER
SISMEMBER用于判断当前给定的元素是否在集合中。
- 语法:
SISMEMBER key member - 时间复杂度:
O(1) - 返回值:1 表示元素在 set 中;0 表示元素不在 set 中或者 key 不存在。
示例:
redis> SADD myset "one" (integer) 1 redis> SISMEMBER myset "one" (integer) 1 redis> SISMEMBER myset "two" (integer) 02.4 获取集合基数:SCARD
SCARD用于获取一个 set 的基数(cardinality),即集合内的元素个数。
- 语法:
SCARD key - 时间复杂度:
O(1) - 返回值:set 内的元素个数。
示例:
redis> SCARD myset (integer) 22.5 随机移除元素:SPOP
SPOP用于从 set 中删除并返回一个或者多个元素。由于 set 内的元素是无序的,所谓“末尾”并不存在,因此SPOP其实是随机删除。官方文档也明确表达了这种随机性:取出哪个元素实际是未定义行为。其底层在源码中针对spop的实现采用了“生成随机数”的方式。
- 语法:
SPOP key [count] - 时间复杂度:
O(N),N 是 count。 - 返回值:取出的元素。如果不写 count,则随机移除一个;如果写了 count,则移除并返回对应个数个元素。
示例:
redis> SADD myset "one" "two" "three" (integer) 3 redis> SPOP myset "one" redis> SPOP myset 2 1) "three" 2) "two"2.6 移动元素:SMOVE
SMOVE命令用于把一个元素从源 set 中取出,再插入到目标 set 中。针对source中存在,destination中不存在的情况,该命令能正常完成删除与插入。如果移动的元素在source中不存在,则返回 0 表示移动失败。
- 语法:
SMOVE source destination member - 时间复杂度:
O(1) - 返回值:1 表示移动成功,0 表示失败。
示例:
redis> SADD myset "one" "two" (integer) 2 redis> SADD myotherset "three" (integer) 1 redis> SMOVE myset myotherset "two" (integer) 12.7 删除指定元素:SREM
SREM用于将指定的一个或多个元素从 set 中删除。
- 语法:
SREM key member [member ...] - 时间复杂度:
O(N),N 是要删除的元素个数。 - 返回值:本次操作删除的元素个数。
示例:
redis> SADD myset "one" "two" "three" (integer) 3 redis> SREM myset "one" (integer) 1 redis> SREM myset "four" (integer) 03 集合间的运算操作
除了集合内部的操作,Redis 集合最强大的功能在于其支持多个集合间的数学运算。这些操作遵循数学中定义好的规则,主要包含交集、并集和差集。
- 交集(inter):最终结果同时出现在两个集合中。
- 并集(union):把多个集合中的数据都集中放在一起,如果元素有重复,也最终只保留一份。
- 差集(diff):求出那些在集合 A 中存在,同时在集合 B 中不存在的元素。
Redis Set 的三个 STORE 后缀命令具有相同的目标键处理规则:结果非空时创建或覆盖 destination;结果为空时删除 destination,不会保留空集合。
3.1 交集运算:SINTER 与 SINTERSTORE
SINTER用于获取给定 set 的交集中的元素并直接返回数据。
- 语法:
SINTER key [key ...] - 时间复杂度:
O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。 - 返回值:交集的元素。
示例:
redis> SADD key1 "a" "b" "c" (integer) 3 redis> SADD key2 "c" "d" "e" (integer) 3 redis> SINTER key1 key2 1) "c"SINTERSTORE则不会直接返回交集的数据,而是直接将把交集的结果保存到destination对应的集合中。SINTERSTORE 会在交集结果非空时自动创建 destination,如果结果为空,则不会保留该键。
- 语法:
SINTERSTORE destination key [key ...] - 时间复杂度:
O(N * M)。 - 返回值:返回值为交集的元素个数(Integer)。
示例:
redis> SINTERSTORE key key1 key2 (integer) 1 redis> SMEMBERS key 1) "c"3.2 并集运算:SUNION 与 SUNIONSTORE
SUNION用于获取给定 set 的并集中的元素。
- 语法:
SUNION key [key ...] - 时间复杂度:
O(N),N 指给定的所有集合的总的元素个数。 - 返回值:并集的元素。
示例:
redis> SUNION key1 key2 1) "a" 2) "c" 3) "e" 4) "b" 5) "d"SUNIONSTORE将并集的结果存储到destination对应的集合中。
- 语法:
SUNIONSTORE destination key [key ...] - 时间复杂度:
O(N)。 - 返回值:并集的元素个数。
示例:
redis> SUNIONSTORE key4 key1 key2 (integer) 5 redis> SMEMBERS key4 1) "a" 2) "c" 3) "e" 4) "b" 5) "d"3.3 差集运算:SDIFF 与 SDIFFSTORE
SDIFF用于获取给定 set 的差集中的元素。需要注意的是,差集运算与 key 的顺序相关,SDIFF key1 key2与SDIFF key2 key1的结果完全不同。
- 语法:
SDIFF key [key ...] - 时间复杂度:
O(N)。 - 返回值:差集的元素。
Redis 的SDIFF(差集)就是第一个集合减去后面所有集合中的元素。
SADD set1 a b c d SADD set2 b c SADD set3 c d SDIFF set1 set2 set3计算过程:
set1初始元素:a b c d- 减去
set2中的b c,剩余:a d - 再减去
set3中的c d,剩余:a
最终结果:a
注意:
SDIFF的结果只取决于第一个集合及后续集合的排除作用,后续集合之间不需要计算差集。集合顺序会影响结果,例如SDIFF set2 set1的结果为空。
SDIFFSTORE将差集的结果存储到destination对应的集合中。
- 语法:
SDIFFSTORE destination key [key ...] - 时间复杂度:
O(N)。 - 返回值:差集的元素个数。
示例:
redis> SDIFFSTORE key5 key1 key2 (integer) 2 redis> SMEMBERS key5 1) "a" 2) "b"第二章 深入探究 Redis 集合的内部编码与底层实现
Redis 作为一个卓越的内存数据库,对内存空间的利用可谓精打细算。为了在保证性能的同时尽可能节省内存,Redis 为集合(Set)类型设计了两种不同的底层内部编码:intset(整数集合)和hashtable(哈希表)。在开发与运维过程中,我们可以通过OBJECT ENCODING key命令来实时查看某个集合键的底层编码类型。
本章将详细解析这两种编码的触发条件及其背后的设计逻辑。
1 节省内存的利器:intset(整数集合)
intset是 Redis 为了节省内存空间而做出的特定优化。当集合满足特定条件时,Redis 会选用intset来作为集合的内部实现,从而大幅度减少内存的使用。
intset 本质上是一块连续的内存空间,其内部结构非常紧凑。它包含一个记录编码方式的 encoding 字段、一个记录元素个数的 length 字段,以及一个真正存放数据的整数数组 contents。
+-------------------------+|intset|+-------------------------+|encoding(编码方式)|<--例如:INTSET_ENC_INT16|length(元素个数)|<--例如:4|contents[](整数数组)|<--真正存放数据的连续内存空间+-------------------------+|v+-------+-------+-------+-------+|1|2|3|4|<--元素紧挨着排列,无额外指针开销+-------+-------+-------+-------+触发条件:
集合中的元素全部都是整数。- 集合中元素的个数小于
set-max-intset-entries配置(该配置项默认值为 512 个)。
2 通用且灵活的实现:hashtable(哈希表)
当集合类型无法满足intset的严苛条件时,Redis 会退化并使用hashtable(哈希表)作为集合的内部实现。哈希表能够支持任意类型的字符串元素,并且支持海量数据的存储。
Redis 的 hashtable 底层由字典(dict)结构实现,为了保证扩容时的性能,它内部维护了两个哈希表(用于渐进式 Rehash)。每个哈希表由一个数组构成,数组的每个槽位指向一个链表(或红黑树,视具体 Redis 版本而定),用于解决哈希冲突。
+-------------------------+|dict|+-------------------------+|type(类型)||privdata(私有数据)||ht[2](两个哈希表)|<--通常使用 ht[0],ht[1]用于渐进式 rehash+-------------------------+|v+-------------------------+|dictht|<--哈希表结构+-------------------------+|table(哈希节点数组)|---+|size(哈希表总大小)|||sizemask(掩码)|||used(已有节点数量)||+-------------------------+|v+------+------+------+------+------+------+|*|*|NULL|*|NULL|NULL|<--哈希桶数组+--|---+--|---+------+--|---+------+------+|||v v v+--------+--------++--------+|Entry|Entry||Entry|+--------+--------++--------+|key|key||key|<--拉链法解决哈希冲突|value|value||value||next|next|-->|next|-->NULL+--------+--------++--------+以下两种情况会触发内部编码转换为hashtable:
情况一:元素个数超过配置上限
当集合中的元素个数超过set-max-intset-entries配置的阈值(默认 512 个)时,即使元素全部是整数,底层编码也会自动转换为hashtable,以保证操作的性能。
127.0.0.1:6379> sadd setkey 1 2 3 4 ... (此处省略,共计添加513个元素) (integer) 513 127.0.0.1:6379> object encoding setkey "hashtable"情况二:存在非整数元素
一旦向集合中加入了非整数类型的元素(如字符串),intset的条件被破坏,Redis 会立即将内部编码切换为hashtable。
127.0.0.1:6379> sadd setkey "hello" (integer) 1 127.0.0.1:6379> object encoding setkey "hashtable"第三章 集合的典型应用场景与架构实践
在现代互联网架构中,Redis 集合(Set)凭借其天然去重和高效关系运算的特性,成为了解决许多复杂业务场景的利器。本章将打破单一视角,从用户画像、社交关系与数据统计三个维度,深入探讨集合在实际工程中的广泛应用。
1 核心应用:构建海量用户画像与标签系统
集合类型最典型的使用场景是标签(Tag)系统。在各大互联网平台中,分析用户特征、打标签是精细化运营的基础。
1.1 从用户特征到标签化存储
系统通常会根据用户的历史行为、注册信息(如性别、年龄、居住地、爱好等)来推断用户特征。由于不同用户群体的商业价值存在显著差异(例如在消费市场中常说的“女人 > 孩子 > 老人 > 狗 > 男人”),精准的标签能帮助电商网站做差异化的产品推荐。
互联网大厂之间常通过手机号、微信号等唯一标识共享上述用户数据。当系统通过多渠道将零散的特征收集完毕后,就会将它们转化为简短的字符串,即“标签”,并持久化到 Redis 的 Set 中。用户画像的构建是一件极其复杂且专业的工作,通常由专门的团队负责。
1.2 标签系统的双向索引与操作
在实际存储中,为了满足多种查询需求,我们通常会维护双向的集合映射:既记录用户拥有哪些标签,也记录某个标签下有哪些用户。
1. 给用户添加标签(正向索引)
sadd user:1:tags tag1 tag2 tag5 sadd user:2:tags tag2 tag3 tag5 ... sadd user:k:tags tag1 tag2 tag42. 给标签添加用户(反向索引)
sadd tag1:users user:1 user:3 sadd tag2:users user:1 user:2 user:3 ... sadd tagk:users user:1 user:4 user:9 user:283. 删除维护关系
当用户取消兴趣或标签失效时,需要同步清理双向数据:
# 删除用户下的标签 srem user:1:tags tag1 tag5 # 删除标签下的用户 srem tag1:users user:1 srem tag5:users user:11.3 标签衍生出的“用户关系”与信息茧房
基于上述标签数据,我们可以进行更深度的挖掘。例如,通过求交集运算计算两个用户的共同兴趣:
sinter user:1:tags user:2:tags基于这种共同兴趣,很容易找到两个用户之间的公共标签,进而衍生出“用户关系”(如互相推荐可能认识的人)。
在推荐系统中,这种基于标签的玩法被短视频等平台发挥到了极致。当你认真观看了某类视频后,服务器就会通过标签判定你的喜好,接下来不断推送同类内容。这在客观上造成了“信息茧房”效应:你看到的东西,都是你愿意看到的;你不愿意看的,就很难被看到。用户看到的内容始终是一个“小圈子”,到处传递的都是你关注领域的焦点信息。
2 社交网络:基于集合交集的好友推荐
除了标签,集合在社交软件(如 QQ、微信)中计算共同好友的场景也同样经典。基于“集合求交集”的运算,系统可以轻松地做出好友推荐。
假设存在以下社交关系:
- A 和 B 是好友
- A 和 C 是好友
- B、C 都和 D 是好友
当我们需要给 A 推荐好友时,系统可以提取 A 的好友集合与 D 的好友集合,求其交集。发现 A 和 D 存在共同的交集(B 和 C),系统就会根据社交图谱的亲密度,将 D 推荐给 A。这种依赖集合底层高效运算的推荐算法,极大地提升了用户体验。
3 流量统计:利用天然去重特性计算 UV
在互联网产品运营中,衡量用户规模的两个核心指标是 PV 和 UV。
- PV (Page View):页面浏览量。用户每次访问该服务器,每次访问都会产生一个 PV。
- UV (User View):独立访客数。每个用户访问服务器,会产生一个 UV。但同一个用户多次访问,不会使 UV 增加。
由于 UV 需要严格按照用户进行去重,这与 Set 集合的唯一性完美契合。在数据量可控的情况下,我们可以利用 Set 来记录当天访问的用户 ID。无论用户一天内刷新多少次页面,由于 Set 的特性,存入的都是同一个元素,最终通过SCARD命令获取集合的基数,就能精准统计出当天的 UV 数据,完美解决了去重统计的需求。