Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1351 字
7 分钟
Redis学习系列 | 共同好友原来这么算

去重的力量#

前面写了 String、Hash、List。到 Set 这里,它有一个其他类型都没有的特性:自动去重,而且判断一个元素在不在集合里是 O(1)

这个特性单独看没什么,但放到具体场景里就厉害了。点赞列表不能重复、标签系统不能重名、抽奖不能一个人中两次——这些”不能重复”的需求,用 Set 天然解决,不需要额外写查重逻辑。

Set 的核心操作就几个:

SADD tags:post:42 "Redis" "Java" "数据库" # 添加元素,重复的自动忽略
SREM tags:post:42 "Java" # 删除
SISMEMBER tags:post:42 "Redis" # 是否存在 → 1
SCARD tags:post:42 # 元素个数 → 2

这些都好懂。Set 真正有用的是集合运算——交集、并集、差集。这三种运算组合起来,能处理社交、标签、统计、推荐里大量常见需求。

三个运算,三种业务#

交集 SINTER:共同好友#

社交产品里”共同好友”是怎么算出来的?

# 刘备的好友
SADD friends:liubei "zhaoyun" "guanyu" "zhangfei" "diaochan"
# 曹操的好友
SADD friends:caocao "diaochan" "xiahoudun" "dianwei" "zhangliao"
# 他俩的共同好友
SINTER friends:liubei friends:caocao
# → ["diaochan"]

每个用户的好友存一个 Set。查共同好友就是取两个 Set 的交集,Redis 帮你算完,O(N*M) 的复杂度在服务端完成——不需要把数据拉到客户端自己算。

SINTERSTORE 可以把结果缓存下来:

SINTERSTORE common:liubei_caocao friends:liubei friends:caocao
# 共同好友结果存成一个新 Set,下次直接读

差集 SDIFF:可能认识的人#

QQ 的”可能认识的人”推荐,逻辑是这个:

王五的好友里,有哪些不是我的好友?

# 我的好友
SADD friends:me "101" "102" "103"
# 王五的好友
SADD friends:wangwu "102" "103" "104" "105"
# 王五的好友 - 我的好友 = 我可能认识的人
SDIFF friends:wangwu friends:me
# → ["104", "105"]

并集 SUNION:全量标签#

一个文章有多个标签,查”包含标签 A 或标签 B 的所有文章”:

# 标签到文章的反向索引
SADD tag:redis "post:1" "post:3" "post:42"
SADD tag:java "post:1" "post:2" "post:42"
# 包含"redis"或"java"的文章
SUNION tag:redis tag:java
# → ["post:1", "post:2", "post:3", "post:42"]

标签筛选的组合也很自然——交集找同时包含多个标签的文章(AND),并集找包含任一标签的文章(OR)。

抽奖:随机,但别重复#

Set 有两个随机读取的命令,区别很重要:

# SPOP:随机弹出并删除(不重复抽奖)
SADD lucky_draw "user:1" "user:2" "user:3" ... "user:100"
SPOP lucky_draw 3
# 随机返回 3 个用户,并且从池子里删掉了 → 不会重复中奖
# SRANDMEMBER:随机读取,不删除(可重复展示)
SRANDMEMBER lucky_draw 3
# 随机返回 3 个,用户还在池子里 → 下次还有可能被抽到

写抽奖系统时最怕的就是一人中两次——不是因为随机算法不好,而是因为没有在中奖后把用户移除。用 SPOP 直接在 Redis 层面就杜绝了这个问题。

Set 内部怎么省内存#

Hash 用了 listpack,Set 也有自己的内存优化:intset

如果 Set 的所有元素都是整数,且数量不超过 512(默认 set-max-intset-entries),Redis 不用哈希表存,而是用一个有序整型数组:

intset 内存布局:[enc][len][elem1][elem2][elem3]...

所有整数紧密排列在一块连续内存里,没有指针,没有哈希桶。比标准哈希表省大约 60% 内存。

但只要有一个元素不是整数,或者数量超过 512,intset 就会自动升级成 hashtable。注意这个升级是不可逆的——即使你后面把非整数元素全删了,Set 也不会回到 intset。所以如果存的是纯数字 ID,尽量保持数量在阈值以内。

SADD numbers 1 2 3 4 5
OBJECT ENCODING numbers
# → "intset"(紧凑)
SADD numbers "hello"
OBJECT ENCODING numbers
# → "hashtable"(升级了,回不去了)

两个容易踩的坑#

坑一:SMEMBERS 大集合#

SMEMBERS 返回整个集合,如果集合有几万甚至几十万元素,会严重阻塞 Redis 的单线程。

正确的做法是 SSCAN 分批拉:

cursor = 0
while True:
cursor, members = redis.sscan("big_set", cursor, count=100)
for m in members:
process(m)
if cursor == 0:
break

坑二:集合运算的复杂度#

SINTERSUNIONSDIFF 的复杂度是 O(N*M),N 和 M 分别是两个 Set 的大小。两个 10 万元素的 Set 做交集,运算本身可能耗时几百毫秒——在单线程模型下,这段时间整个 Redis 都不能处理其他请求。

解法:用 SINTERSTORE / SUNIONSTORE / SDIFFSTORE 把结果存到新 key,后续直接读,不用每次重算。或者在大数据量场景下,把计算挪到从节点、甚至客户端做。

总结#

Set 的核心价值就一个字——

单个 Set 解决去重和存在性判断。多个 Set 配合交集、并集、差集,把”集合运算”这件事在 Redis 服务端做完,不用拉数据到客户端算。

最重要的三个口诀:

需求命令口诀
共同好友SINTER你有、我也有
可能认识SDIFF他有、我没有
随机不重复SPOP抽完就走

下一篇聊 Sorted Set——每个元素带一个 score,自动排序。排行榜、延迟队列、优先级任务,都在它身上。


这是 Redis 学习系列的第 5 篇。下一篇:Redis学习系列 | 实时排名到底怎么做的

Redis学习系列 | 共同好友原来这么算
https://qiandaos.top/posts/redis-learning-series/5-set/
作者
千岛寒流
发布于
2025-06-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00