去重的力量
前面写了 String、Hash、List。到 Set 这里,它有一个其他类型都没有的特性:自动去重,而且判断一个元素在不在集合里是 O(1)。
这个特性单独看没什么,但放到具体场景里就厉害了。点赞列表不能重复、标签系统不能重名、抽奖不能一个人中两次——这些”不能重复”的需求,用 Set 天然解决,不需要额外写查重逻辑。
Set 的核心操作就几个:
SADD tags:post:42 "Redis" "Java" "数据库" # 添加元素,重复的自动忽略SREM tags:post:42 "Java" # 删除SISMEMBER tags:post:42 "Redis" # 是否存在 → 1SCARD 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 5OBJECT ENCODING numbers# → "intset"(紧凑)
SADD numbers "hello"OBJECT ENCODING numbers# → "hashtable"(升级了,回不去了)两个容易踩的坑
坑一:SMEMBERS 大集合
SMEMBERS 返回整个集合,如果集合有几万甚至几十万元素,会严重阻塞 Redis 的单线程。
正确的做法是 SSCAN 分批拉:
cursor = 0while True: cursor, members = redis.sscan("big_set", cursor, count=100) for m in members: process(m) if cursor == 0: break坑二:集合运算的复杂度
SINTER、SUNION、SDIFF 的复杂度是 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学习系列 | 实时排名到底怎么做的
部分信息可能已经过时





