深入剖析Redis系列(八) - Redis数据结构之集合 青旅半醒 2022-03-15 07:00 240阅读 0赞 # 前言 # **集合**(`set`)类型也是用来保存多个 **字符串元素**,但和 **列表类型** 不一样的是,集合中 **不允许有重复元素**,并且集合中的元素是 **无序的**,不能通过 **索引下标** 获取元素。 ![12738336-89505c8fa9b1444e][] image 如图所示,集合 `user:1:follow` 包含着 `"it"`、`"music"`、`"his"`、`"sports"` 四个元素,一个 **集合** 最多可以存储 `2 ^ 32 - 1` 个元素。`Redis` 除了支持 **集合内** 的 **增删改查**,同时还支持 **多个集合** 取 **交集**、**并集**、**差集**。合理地使用好集合类型,能在实际开发中解决很多实际问题。 ![12738336-19cef1a0c175d83f][] image # 正文 # ## 1. 相关命令 ## ### 1.1. 集合内的操作命令 ### #### 1.1.1. 添加元素 #### > sadd key element \[element ...\] 返回结果为添加成功的 **元素个数**,例如: 127.0.0.1:6379> exists myset (integer) 0 127.0.0.1:6379> sadd myset a b c (integer) 3 127.0.0.1:6379> sadd myset a b (integer) 0 ### 1.1.2. 删除元素 ### > srem key element \[element ...\] 返回结果为成功删除 **元素个数**,例如: 127.0.0.1:6379> srem myset a b (integer) 2 127.0.0.1:6379> srem myset hello (integer) 0 #### 1.1.3. 计算元素个数 #### > scard key `scard` 的 **时间复杂度** 为 `O(1)`,它 **不会遍历** 集合所有元素,而是直接用 `Redis` 的 **内部** 的变量,例如: 127.0.0.1:6379> scard myset (integer) 1 #### 1.1.4. 判断元素是否在集合中 #### > sismember key element 如果给定元素 `element` 在集合内返回 `1`,反之返回 `0`。 127.0.0.1:6379> sismember myset c (integer) 1 #### 1.1.5. 随机从集合返回指定个数元素 #### > srandmember key \[count\] `[count]` 是 **可选参数**,如果不写默认为 `1`。 127.0.0.1:6379> srandmember myset 2 1) "a" 2) "c" 127.0.0.1:6379> srandmember myset "d" #### 1.1.6. 从集合随机弹出元素 #### > spop key `spop` 操作可以从 **集合** 中 **随机弹出** 一个元素,例如下面代码是一次 `spop` 后,**集合元素** 变为 `"d b a"`。 127.0.0.1:6379> spop myset "c" 127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a" > **注意**:`Redis` 从 `3.2` 版本开始, `spop` 也支持 `[count]` 参数。 `srandmember` 和 `spop` 都是 **随机** 从集合选出元素,两者不同的是 `spop` 命令执行后,**元素** 会从集合中 **删除**,而 `srandmember` 不会删除元素。 #### 1.1.7. 获取所有元素 #### > smembers key 下面代码获取集合 `myset` 的 **所有元素**,并且 **返回结果** 是 **无序的**。 127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a" `smembers` 和 `lrange`、`hgetall` 都属于 **比较重** 的命令,如果 **元素过多** 存在 **阻塞** `Redis` 的可能性,这时候可以使用 `sscan` 命令来完成。 ### 1.2. 集合间的操作命令 ### 现在有 **两个集合**,它们分别是 `user:1:follow` 和 `user:2:follow`。 127.0.0.1:6379> sadd user:1:follow it music his sports (integer) 4 127.0.0.1:6379> sadd user:2:follow it news ent sports (integer) 4 #### 1.2.1. 求多个集合的交集 #### > sinter key \[key ...\] 下面的代码是求 `user:1:follow` 和 `user:2:follow` 两个集合的 **交集**,返回结果是 `sports`、`it`。 127.0.0.1:6379> sinter user:1:follow user:2:follow 1) "sports" 2) "it" #### 1.2.2. 求多个集合的并集 #### > suinon key \[key ...\] 下面的代码是求 `user:1:follow` 和 `user:2:follow` 两个集合的 **并集**,返回结果是 `sports`、`it`、`his`、`news`、`music`、`ent`。 127.0.0.1:6379> sunion user:1:follow user:2:follow 1) "sports" 2) "it" 3) "his" 4) "news" 5) "music" 6) "ent" #### 1.2.3. 求多个集合的差集 #### > sdiff key \[key ...\] 下面的代码是求 `user:1:follow` 和 `user:2:follow` 两个集合的 **差集**,返回结果是 `music`和 `his`。 127.0.0.1:6379> sdiff user:1:follow user:2:follow 1) "music" 2) "his" 前面三个求 **交集**、**并集** 和 **差集** 的操作得到的结果,如图所示: ![12738336-4159a53a2e665f0e][] image #### 1.2.4. 将交集、并集、差集的结果保存 #### **集合间** 的运算在 **元素较多** 的情况下会 **比较耗时**,所以 `Redis` 提供了以下 **三个命令**(**原命令** \+ `store`)将 **集合间交集**、**并集**、**差集** 的结果保存在 `destination key` 中。 > sinterstore destination key \[key ...\] > suionstore destination key \[key ...\] > sdiffstore destination key \[key ...\] 下面的操作会将 `user:1:follow` 和 `user:2:follow` **两个集合** 的 **交集结果** 保存在 `user:1_2:inter` 中,`user:1_2:inter` 本身也是 **集合类型**。 127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow (integer) 2 127.0.0.1:6379> type user:1_2:inter set 127.0.0.1:6379> smembers user:1_2:inter 1) "it" 2) "sports" 有关 **集合** 的 **常用命令** 基本上就是这么多了,下表给出了 **集合常用命令** 的 **时间复杂度**,开发人员可以根据自身需求进行选择。 ![12738336-9d263c0608c3d1f4][] image ## 2. 内部编码 ## **集合类型** 的 **内部编码** 有两种: ### 2.1. intset(整数集合) ### 当集合中的元素都是 **整数** 且 **元素个数** 小于 `set-max-intset-entries` 配置(默认 `512` 个)时,`Redis` 会选用 `intset` 来作为 **集合** 的 **内部实现**,从而 **减少内存** 的使用。 ### 2.2. hashtable(哈希表) ### 当集合类型 **无法满足** `intset` 的条件时,`Redis` 会使用 `hashtable` 作为集合的 **内部实现**。 ### 2.3. 具体示例 ### #### 2.3.1. 内部编码为整数集合 #### 当元素个数 **较少** 且都为 **整数** 时,**内部编码** 为 `intset`。 127.0.0.1:6379> sadd setkey 1 2 3 4 (integer) 4 127.0.0.1:6379> object encoding setkey "intset" #### 2.3.2. 内部编码为哈希 #### * 当 **元素个数** 超过 `512` 个,**内部编码** 变为 `hashtable`。 127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513 (integer) 513 127.0.0.1:6379> scard setkey (integer) 513 127.0.0.1:6379> object encoding listkey "hashtable" * 当某个元素 **不为整数** 时,**内部编码** 也会变为 `hashtable`。 127.0.0.1:6379> sadd setkey a (integer) 1 127.0.0.1:6379> object encoding setkey "hashtable" 有关集合类型的 **内存优化** 技巧,后面的文章会有专门的讲解。 ## 3. 应用场景 ## ### 3.1. 用户标签 ### **集合类型** 比较典型的使用场景是 **标签**(`tag`)。简单的举两个例子: #### 3.1.3. 娱乐新闻推荐 #### > 一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。 #### 3.1.2. 电商人群分类 #### > 一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。 下面使用 **集合类型** 实现 **标签功能**。 * **给用户添加标签** sadd user:1:tags tag1 tag2 tag5 sadd user:2:tags tag2 tag3 tag5 ... sadd user:k:tags tag1 tag2 tag4 ... * **给标签添加用户** sadd tag1:users user:1 user:3 sadd tag2:users user:1 user:2 user:3 ... sadd tagk:users user:1 user:2 ... > **用户** 和 **标签** 的 **关系维护** 应该在 **一个事务内** 执行,防止 **部分命令失败** 造成的 **数据不一致**,有关如何将 **两个命令** 放在 **一个事务** 中,后面会介绍 `Lua` 的使用。 * **删除用户下的标签** srem user:1:tags tag1 tag5 ... * **删除标签下的用户** srem tag1:users user:1 srem tag5:users user:1 ... * **计算用户共同感兴趣的标签** 可以使用 `sinter` 命令,来计算 **用户共同感兴趣** 的 **标签**。 sinter user:1:tags user:2:tags 上面只是给出了使用 `Redis` **集合类型** 实现 **标签** 的基本思路,实际上一个 **标签系统** 远比这个要 **复杂** 得多,不过 **集合类型** 的 **应用场景** 通常为以下几种: <table> <thead> <tr> <th style="text-align:left;">命令组合</th> <th style="text-align:left;">应用场景</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">sadd</td> <td style="text-align:left;">Tagging(标签)</td> </tr> <tr> <td style="text-align:left;">spop/srandmember</td> <td style="text-align:left;">Random item(生成随机数,比如抽奖)</td> </tr> <tr> <td style="text-align:left;">sadd + sinter</td> <td style="text-align:left;">Social Graph(社交需求)</td> </tr> </tbody> </table> # 小结 # 本文介绍了 `Redis` 中的 **集合** 的 一些 **基本命令**,包括 **集合内部的操作命令** 和 **集合之间的操作命令**,其次还介绍了 **集合** 的 **内部编码** 转换和以 **用户行为标签** 为主的 **应用场景**。 # 参考 # 《Redis 开发与运维》 -------------------- 欢迎关注技术公众号: 零壹技术栈 ![12738336-0093da3654270579][] 零壹技术栈 本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。 [12738336-89505c8fa9b1444e]: /images/20220315/d3e904e5f9474d0dabec660d331aa4c5.png [12738336-19cef1a0c175d83f]: /images/20220315/5e4dd0e00cfd47c9bf1aff90729af28b.png [12738336-4159a53a2e665f0e]: /images/20220315/deedc9bbd1344e77adecaf0037872c48.png [12738336-9d263c0608c3d1f4]: /images/20220315/e76bb788aecc405a8996b980d1832efe.png [12738336-0093da3654270579]: /images/20220315/c6269a689f054b9db96eeb0484f5b0eb.png
还没有评论,来说两句吧...