文章目录
简介
Redis 的 Set 是 String 类型的无序集合。集合中任意成员是唯一的,即集合中不能出现重复的数据。
-
特点:
- 无序性:集合中的元素没有特定的顺序。
- 唯一性:集合中不允许重复的元素。
- 元素数量限制:最多可以存储2^32-1个元素。
-
基本操作:
- 增加元素:向集合中添加一个元素,如果元素已存在则忽略。
- 删除元素:从集合中移除指定元素。
- 检查元素是否存在:判断集合中是否存在某个元素。
-
集合运算:
- 并集:返回多个集合的所有成员的并集。
- 交集:返回多个集合的所有成员的交集。
- 差集:返回指定集合和其他集合之间的差集。
-
应用场景:
- 去重:适用于需要存储唯一值的场景。
- 标签管理:管理对象的标签集合。
- 社交网络关系处理:例如找出共同好友等操作。
Set类型在标签管理中键值对关系。
Set 和 List的区别
在Redis中,Set(集合)和List(列表)是两种不同的数据结构,在数据结构和操作特性有以下区别:
- 数据结构特性:
- List(列表): 是一个有序的字符串链表。列表中的每个元素都包含一个字符串值。
- Set(集合): 是一个不重复且无序的字符串集合。集合中的每个元素都是唯一的,不存在重复的元素。
- 插入顺序:
- List: 元素按照插入顺序排序,可以在列表的两端进行插入(左端和右端)。
- Set: 元素没有特定的顺序,Redis会自动为集合中的元素建立索引以提高查找速度。
- 支持的操作:
- List: 支持按索引获取元素,支持在列表两端的插入和删除操作,支持修剪(Trim)操作,可以用作栈(LIFO)或队列(FIFO)。
- Set: 支持添加、移除和检查元素是否存在,支持集合间的交集、并集和差集等操作。
常用命令
增删改查类命令
添加元素
# 添加元素:向集合 key 中添加一个或多个成员元素。
SADD key member [member ...]
向集合 key 中添加一个或多个成员元素,下列例子中,构建一个user:ikun
的用户关键字,把他的爱好放入到他的set
集合中。
移除元素
# 从集合 key 中移除一个或多个成员元素。
SREM key member [member ...]
通过SREM
指令移除(remove)一个或多个元素,比如移除user:ikun:hobby
的BasketBall
这样集合中就不再拥有篮球这个爱好了,说明这个人就是假fans(doge),怎么检验是不是假fans呢?
判断元素是否存在
# 判断成员元素 member 是否存在于集合 key 中,返回布尔值。
SISMEMBER key member
通过SISMEMBER
指令判断member
元素是否是key
集合的成员(is member),例如查询user:ikun:hobby
喜不喜欢打篮球,可以用下面这个例子
最终返回一个0
表示false
表示不存在
获取集合大小
# 返回集合 key 的基数(集合中元素的数量)。
SCARD key
通过SCARD
获取集合的基数(Cardinality),即统计集合元素的数量
获取集合所有成员
# 获取集合所有成员
SMEMBERS key
通过SMEMBERS
获取key
中的所有成员(Members),如下图示例:
随机获取元素
# 返回集合 key 中一个或多个随机成员。如果指定了 count 参数且为正数,则返回多个随机成员;如果为负数,则会返回1个成员。
SRANDMEMBER key [count]
通过SRANDMEMBER
返回集合key
中一个或多个随机成员(rand member)。如果指定了 count
参数且为正数,则返回多个随机成员;如果为负数,则会返回1个成员。此方法比较适合抽奖操作。
随机移除并返回元素
# 移除并返回集合 key 中的一个或多个随机成员。与 SRANDMEMBER 不同的是,SPOP 会从集合中移除元素。
# 3.2 + 版本支持[count]命令
SPOP key [count]
通过SPOP
弹出(Pop)并返回集合 key
中的一个或多个随机成员,与SRANDMEMBER
不同的是,该指令会彻底删除元素,适合不重复抽奖的场景。
运算操作命令
集合间操作
# 返回给定多个集合的并集
SUNION key [key ...]
# 返回给定多个集合的交集
SINTER key [key ...]
# 返回给定多个集合的差集
SDIFF key [key ...]
三者分别是数学中的并、交、差运算,其运算模式如下图:
首先构建两个集合
- 并集
- 交集
- 差集
集合间操作并存储
# 计算给定多个集合的并集,并将结果存储在 destination 集合中。
SUNIONSTORE destination key [key ...]
# 计算给定多个集合的交集,并将结果存储在 destination 集合中。
SINTERSTORE destination key [key ...]
# 计算给定多个集合的差集,并将结果存储在 destination 集合中。
SDIFFSTORE destination key [key ...]
跟集合操作命令类似,唯一的区别就是就是会进行额外的存储记录操作。
上图就是一个使用并集并存储的例子,可以看到store
也会作为一个集合存入到Redis中。
应用场景
集合(Set)具有无序、元素不可重复、支持并集、交集、差集等操作的特性。
因此,它非常适合用来存储需要保证元素唯一性且无序的数据。例如,在需要进行数据去重的场景下,集合类型能够很好地满足需求。此外,集合还能方便地统计多个集合之间的并集、交集和差集,这对于数据分析和处理非常有用。
然而,需要注意的是,集合类型在执行并集、交集、差集等操作时,计算复杂度较高。特别是在数据量较大的情况下,直接在Redis主库上执行这些操作可能会导致Redis实例阻塞,影响其他请求的处理效率。
为了避免主库因为集合类型的聚合计算而阻塞,可以考虑以下策略:
- 在主从集群中,选择一个从库来执行集合的聚合统计操作。
- 将集合数据返回给客户端,由客户端程序来完成聚合统计,减轻主库的压力。
通过这些策略,可以有效地管理和利用Redis中集合类型的强大功能,并保持业务系统的稳定性和高效性。
博客点赞
用户点赞操作
article:1
表示文章的标识,user:x
表示任意用户,每个用户通过点击点赞按钮触发Redis调用SADD
命令,从而使用户加入到文章的点赞集合中。大家可以试一试这个加入到点赞集合过程。
类似的,用户可以通过SREM
命令实现点赞的取消,咳咳,这个就不用试了。
当然点赞的右边还有当前的点赞数量,这个可以使用SCAR
命令返回对应的点赞数量。
公众号共同关注
在Redis中使用Set来实现公众号的共同关注场景是非常合适的。假设有多个用户,每个用户可以关注多个公众号,我们希望找出同时被多个用户关注的公众号,即求公众号的交集。
用户关注集合
使用Redis的Set来存储每个用户关注的公众号。
SADD user:1:subscriptions 1 3 4 5 8
SADD user:2:subscriptions 2 4 5 7 8
共同关注查询
通过set的集合运算功能实现关注查询
共同关注的公众号
SINTER user:1:subscriptions user:2:subscriptions
通过交集查询共同关注的公众号。
给user:2
推荐user:1
的号
SDIFF user:1:subscriptions user:2:subscriptions
通过SDIFF
命令查询差集给对方推荐公众号
抽奖活动
通过SRANDMEMBER
或SPOP
命令实现随机选取一个或多个成员进行抽奖。
SADD lottery:participants Alice James Emily William Sophia Benjamin John Sean Marry
将参与成员加入到奖池参与名单中,以备之后抽奖使用
# 随机抽1个人中 一等奖
SRANDMEMBER lottery:participants 1
1) "John"
# 随机抽2个人中 二等奖
SRANDMEMBER lottery:participants 2
1) "William"
2) "John"
# 随机抽3个人中 三等奖
SRANDMEMBER lottery:participants 3
1) "Alice"
2) "John"
3) "James"
使用SRANDMEMBER
进行抽奖有一个坏处,就是会出现天选之子,比如上面的John,连续三次中奖,所以也可以使用另一个命令SPOP
来进行去重抽奖,中奖后弹出集合不再进行抽奖。
# 随机抽1个人中 一等奖
SPOP lottery:participants 1
1) "William"
# 随机抽2个人中 二等奖
SPOP lottery:participants 2
1) "James"
2) "Benjamin"
# 随机抽3个人中 三等奖
SPOP lottery:participants 3
1) "Emily"
2) "John"
3) "Sean"
内部实现
Set类型有两种底层数据结构:整数集合和哈希表
-
**整数集合(intset):**当集合中的所有元素都是整数,并且元素个数小于等于 512(默认值,可以通过配置 set-max-intset-entries 修改),Redis 会使用整数集合(intset)作为 Set 类型的底层数据结构。
-
**哈希表(hashtable):**如果集合中的元素包含非整数类型的元素,或者整数元素个数超过了整数集合的限制,Redis 将使用哈希表作为 Set 类型的底层数据结构。
整数集合
整数集合(intset)是一种特定的数据结构,用于存储整数值集合,是集合键的底层实现之一。它的设计目标是在内存使用效率和执行效率之间取得良好的平衡。当集合只包含整数元素,且数量小于等于512时,Redis就会使用整数集合作为Set的底层实现。
intset
即为整数集合
整数集合的结构设计
整数集合的整数类型可以int16_t
、int32_t
、int64_t
三种,并且集合中不会出现相同元素,其代码结构设置在intset.h/intset
结构体中,表示如下:
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
contents
即表示Set集合的内容,整数集合的每个元素都是contents
数组的一个数组项,每个数组项在数组中按顺序从大到小排列,并且数组中不会包含重复元素。
length
表示数组contents
的长度。
细心的同学会发现contents
申请的整数类型为int8_t
,但是前面确说数组只可以表示其他三种类型。这是整数集合的巧妙设计之一,int8_t
只是一个基数,实际表示编码的是encoding
属性。contents
的真正类型取决于encoding
的属性值。
encoding 值 | contents 实际类型 | contents 类型范围 |
---|---|---|
INTSET_ENC_INT16 | int16_t | -32768 ~ 32767 |
INTSET_ENC_INT32 | int32_t | -232 ~ 232 - 1 |
INTSET_ENC_INT64 | int64_t | -264 ~ 264 -1 |
上图表示两种类型的整数集合,此时两个集合的长度虽然都是4,但是每个contents
的内存是天差地别。
- int16_t的集合,其数组大小 = sizeof(int16_t) * 4 = 16 * 4 = 64 位
- int64_t的集合,其数组大小 = sizeof(int64_t) * 4 = 64 * 4 = 256 位
由此可想到一个问题,当int16_t的整数集合加入一个int32_t 或 int64_t的整数,这个整数集合会怎么变化的呢?
整数集合的升级
当大整数加入到小整数的集合中,会先进行集合升级的操作,然后再将大整数加入到集合中。
升级的过程可以分为三个步骤
- 根据新元素的大小,扩充整数的
contents
空间,并为新元素分配空间 - 将旧元素按照新元素的位大小放入新的空间,并且再放置过程中,也要维持整数集合的有序性。
- 将新元素加入到数组里面。
如图中例子表示一个int16_t
的整数集合,当加入新元素65535(int32_t
)整数时,集合底层将会进行第一步扩充操作。
计算未来整数集合需要空间为32 * 5 = 160
,所以扩充空间96
位,扩充完数组后,将会进行第二部操作,扩充旧元素并保留顺序。
当扩充完原始数组后,就要执行最后一步即加入新元素。
最后,完成升级的三步操作就要更新集合的表信息。
更新encoding
属性为新的存储类型,更新length
为新的长度。至此就完成了更新操作,当然其他升级也与这个过程类似,这里就不过多演示了。
升级的好处
整数集合的底层实现确实可以根据需要动态升级存储元素的类型,这样可以有效地节省内存资源,避免不必要的内存浪费。具体来说,整数集合的优势包括:
- 提升灵活性: C语言通常是静态类型,使用int16_t数组就只能使用该数组,整数集合的设计使三种整数类型的在使用层面没有切换感知。
- **按需升级:**当需要添加更大范围的整数(如 int32_t 或 int64_t 类型)时,整数集合会动态地将内部数组升级为适合存储更大范围整数的类型,如 int32_t 或 int64_t。这样做可以在需要时扩展存储容量,而不是一开始就分配更大的空间,避免了一开始就浪费内存的情况。
- **节省内存:**这种设计使得整数集合在存储小范围整数时非常紧凑,节省了内存资源。只有在需要存储更大范围的整数时才会有较小的额外开销。
降级
整数集合不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。比如前面的升级操作的例子,如果删除了 65535 元素,整数集合的数组还是 int32_t 类型的,并不会因此降级为 int16_t 类型。
哈希表
哈希表提供了更灵活的存储方式,能够处理任意类型的元素,并且支持动态扩容。该内容在上一篇中Hash表内部实现已经提到了,这里就不再赘述了,如果大家感兴趣的话,请自行查看。
本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!