【Redis 理论与实践学习】 一、Redis的数据结构:4.Set类型


简介

Redis 的 Set 是 String 类型的无序集合。集合中任意成员是唯一的,即集合中不能出现重复的数据。

  • 特点:

    • 无序性:集合中的元素没有特定的顺序。
    • 唯一性:集合中不允许重复的元素。
    • 元素数量限制:最多可以存储2^32-1个元素。
  • 基本操作:

    • 增加元素:向集合中添加一个元素,如果元素已存在则忽略。
    • 删除元素:从集合中移除指定元素。
    • 检查元素是否存在:判断集合中是否存在某个元素。
  • 集合运算:

    • 并集:返回多个集合的所有成员的并集。
    • 交集:返回多个集合的所有成员的交集。
    • 差集:返回指定集合和其他集合之间的差集。
  • 应用场景:

    • 去重:适用于需要存储唯一值的场景。
    • 标签管理:管理对象的标签集合。
    • 社交网络关系处理:例如找出共同好友等操作。

Set类型在标签管理中键值对关系。

Set 和 List的区别

在Redis中,Set(集合)和List(列表)是两种不同的数据结构,在数据结构和操作特性有以下区别:

  1. 数据结构特性:
    • List(列表): 是一个有序的字符串链表。列表中的每个元素都包含一个字符串值。
    • Set(集合): 是一个不重复且无序的字符串集合。集合中的每个元素都是唯一的,不存在重复的元素。
  2. 插入顺序:
    • List: 元素按照插入顺序排序,可以在列表的两端进行插入(左端和右端)。
    • Set: 元素没有特定的顺序,Redis会自动为集合中的元素建立索引以提高查找速度。
  3. 支持的操作:
    • List: 支持按索引获取元素,支持在列表两端的插入和删除操作,支持修剪(Trim)操作,可以用作栈(LIFO)或队列(FIFO)。
    • Set: 支持添加、移除和检查元素是否存在,支持集合间的交集、并集和差集等操作。

常用命令

增删改查类命令

添加元素

# 添加元素:向集合 key 中添加一个或多个成员元素。
SADD key member [member ...]

向集合 key 中添加一个或多个成员元素,下列例子中,构建一个user:ikun的用户关键字,把他的爱好放入到他的set集合中。

移除元素

# 从集合 key 中移除一个或多个成员元素。
SREM key member [member ...]

通过SREM指令移除(remove)一个或多个元素,比如移除user:ikun:hobbyBasketBall

这样集合中就不再拥有篮球这个爱好了,说明这个人就是假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实例阻塞,影响其他请求的处理效率。

为了避免主库因为集合类型的聚合计算而阻塞,可以考虑以下策略:

  1. 在主从集群中,选择一个从库来执行集合的聚合统计操作。
  2. 将集合数据返回给客户端,由客户端程序来完成聚合统计,减轻主库的压力。
    通过这些策略,可以有效地管理和利用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命令查询差集给对方推荐公众号

抽奖活动

通过SRANDMEMBERSPOP命令实现随机选取一个或多个成员进行抽奖。

 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类型有两种底层数据结构:整数集合和哈希表

  1. **整数集合(intset):**当集合中的所有元素都是整数,并且元素个数小于等于 512(默认值,可以通过配置 set-max-intset-entries 修改),Redis 会使用整数集合(intset)作为 Set 类型的底层数据结构。

  2. **哈希表(hashtable):**如果集合中的元素包含非整数类型的元素,或者整数元素个数超过了整数集合的限制,Redis 将使用哈希表作为 Set 类型的底层数据结构。

整数集合

整数集合(intset)是一种特定的数据结构,用于存储整数值集合,是集合键的底层实现之一。它的设计目标是在内存使用效率和执行效率之间取得良好的平衡。当集合只包含整数元素,且数量小于等于512时,Redis就会使用整数集合作为Set的底层实现。

intset即为整数集合

整数集合的结构设计

整数集合的整数类型可以int16_tint32_tint64_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_INT16int16_t-32768 ~ 32767
INTSET_ENC_INT32int32_t-232 ~ 232 - 1
INTSET_ENC_INT64int64_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的整数,这个整数集合会怎么变化的呢?

整数集合的升级

当大整数加入到小整数的集合中,会先进行集合升级的操作,然后再将大整数加入到集合中。
升级的过程可以分为三个步骤

  1. 根据新元素的大小,扩充整数的contents空间,并为新元素分配空间
  2. 将旧元素按照新元素的位大小放入新的空间,并且再放置过程中,也要维持整数集合的有序性。
  3. 将新元素加入到数组里面。

如图中例子表示一个int16_t的整数集合,当加入新元素65535(int32_t)整数时,集合底层将会进行第一步扩充操作。

计算未来整数集合需要空间为32 * 5 = 160 ,所以扩充空间96位,扩充完数组后,将会进行第二部操作,扩充旧元素并保留顺序。

当扩充完原始数组后,就要执行最后一步即加入新元素。

最后,完成升级的三步操作就要更新集合的表信息。


更新encoding属性为新的存储类型,更新length为新的长度。至此就完成了更新操作,当然其他升级也与这个过程类似,这里就不过多演示了。

升级的好处

整数集合的底层实现确实可以根据需要动态升级存储元素的类型,这样可以有效地节省内存资源,避免不必要的内存浪费。具体来说,整数集合的优势包括:

  1. 提升灵活性: C语言通常是静态类型,使用int16_t数组就只能使用该数组,整数集合的设计使三种整数类型的在使用层面没有切换感知。
  2. **按需升级:**当需要添加更大范围的整数(如 int32_t 或 int64_t 类型)时,整数集合会动态地将内部数组升级为适合存储更大范围整数的类型,如 int32_t 或 int64_t。这样做可以在需要时扩展存储容量,而不是一开始就分配更大的空间,避免了一开始就浪费内存的情况。
  3. **节省内存:**这种设计使得整数集合在存储小范围整数时非常紧凑,节省了内存资源。只有在需要存储更大范围的整数时才会有较小的额外开销。

降级

整数集合不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。比如前面的升级操作的例子,如果删除了 65535 元素,整数集合的数组还是 int32_t 类型的,并不会因此降级为 int16_t 类型。

哈希表

哈希表提供了更灵活的存储方式,能够处理任意类型的元素,并且支持动态扩容。该内容在上一篇中Hash表内部实现已经提到了,这里就不再赘述了,如果大家感兴趣的话,请自行查看。

本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值