1. Set(集合)
Set 是一个无序, 不重复的字符串集合. 底层在元素数量少且都是整数时使用 intset 编码, 否则使用 hashtable.
Set = 数学里的"集合" -- 放进去的元素自动去重, 没有顺序之分, 支持交集, 并集, 差集运算.
类比现实: 一个签到簿, 同一个人签多少次名字只出现一次.
1.1 基础操作
SADD
SADD key member [member ...]
向集合添加一个或多个元素. 已存在的元素自动忽略. 返回实际新增的数量.
SREM
SREM key member [member ...]
从集合中移除一个或多个元素. 返回实际移除的数量.
SISMEMBER / SMISMEMBER
SISMEMBER key member / SMISMEMBER key member [member ...]
SISMEMBER 判断元素是否在集合中(返回 0 或 1). SMISMEMBER(Redis 6.2+)批量判断多个元素.
|
SMEMBERS / SCARD
SMEMBERS key / SCARD key
SMEMBERS 返回集合的所有元素(无序). SCARD 返回集合的元素数量(O(1)).
|
SMEMBERS 的性能注意
与 HGETALL 类似, 当集合很大时 SMEMBERS 会一次返回全部元素. 大集合应使用 SSCAN 渐进式遍历.
1.2 集合运算
Set 最强大的能力是集合间运算--交集, 并集, 差集, 且都是原子操作.
SINTER / SUNION / SDIFF
SINTER key [key ...] / SUNION key [key ...] / SDIFF key [key ...]
SINTER 交集(共同拥有的); SUNION 并集(合在一起的); SDIFF 差集(第一个有但其他没有的).
|
用韦恩图(Venn Diagram)理解:
SINTER = 两个圆重叠的部分.
SUNION = 两个圆合起来.
SDIFF = 左圆减去重叠部分.
SINTERSTORE / SUNIONSTORE / SDIFFSTORE
SINTERSTORE dest key [key ...] / SUNIONSTORE dest key [key ...] / SDIFFSTORE dest key [key ...]
同上, 但把结果存储到 destination 而非直接返回. 适合结果集较大或需要后续复用的场景.
1.3 随机操作
SRANDMEMBER
SRANDMEMBER key [count]
随机返回集合中的元素但不移除. count > 0: 返回不重复的 count 个; count < 0: 返回可能重复的 |count| 个.
SPOP
SPOP key [count]
随机弹出(返回并移除)一个或多个元素. 抽奖场景的核心命令.
|
1.4 典型应用场景
| 场景 | 实现方式 | 关键命令 |
|---|---|---|
| 标签系统 | 每个实体一个 Set, 元素为标签 | SADD / SREM / SMEMBERS |
| 共同好友/兴趣 | 两个用户的好友 Set 求交集 | SINTER |
| 去重 | UV 统计, 已推荐内容等 | SADD + SISMEMBER |
| 抽奖 | 参与者加入 Set, SPOP 抽取 | SPOP / SRANDMEMBER |
| 黑/白名单 | IP 或用户 ID 存入 Set, SISMEMBER 校验 | SISMEMBER(O(1) 判断) |
| 可能认识的人 | A 的好友 SDIFF B 的好友 → 推荐给 B | SDIFF |
2. Sorted Set(有序集合)
Sorted Set(简称 ZSet)是 Set 的升级版--每个元素关联一个分值(Score), 集合内的元素按 score 从小到大自动排序, 且元素仍然唯一(但不同元素可以有相同 score).
Set = 一袋弹珠(每颗唯一但没顺序).
Sorted Set = 成绩排行榜 -- 每位选手(member)有一个分数(score), 按分数自动排位. 可以瞬间回答"前 10 名是谁", "某人排第几", "60~90 分之间有多少人".
底层实现
Sorted Set 在元素少(<128 个且值短)时使用 ziplist/listpack 编码; 元素多时使用 skiplist + hashtable 双重结构--skiplist 支持范围查询 O(logN), hashtable 支持 O(1) 按 member 查 score. 两者协作让 ZSet 既能排序又能精确查找.
2.1 基础操作
ZADD
ZADD key [NX|XX] [GT|LT] [CH] score member [score member ...]
添加元素及其分值. member 已存在则更新 score. 可选标志: NX 仅新增; XX 仅更新; GT 仅当新 score > 旧 score 时更新; LT 反之; CH 返回变化(新增+更新)数量而非仅新增数量.
ZSCORE / ZRANK / ZREVRANK
ZSCORE key member / ZRANK key member / ZREVRANK key member
ZSCORE 获取分值. ZRANK 获取正序排名(0-based, 分值最小 = 排名 0). ZREVRANK 获取逆序排名(分值最大 = 排名 0).
|
ZINCRBY
ZINCRBY key increment member
对指定 member 的 score 做原子增减. member 不存在时从 0 开始. 排行榜加分的核心命令.
|
ZREM / ZCARD
ZREM key member [member ...] / ZCARD key
ZREM 移除成员. ZCARD 返回集合总数(O(1)).
2.2 范围查询
Sorted Set 的杀手锏--按排名或按分值范围高效查询.
按排名范围
ZRANGE / ZREVRANGE
ZRANGE key min max [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
按排名(下标)返回元素. Redis 6.2+ 的 ZRANGE 统一了所有范围查询能力(通过 BYSCORE/BYLEX/REV 标志). 加 WITHSCORES 同时返回分值.
|
按分值范围
ZRANGEBYSCORE / ZREVRANGEBYSCORE
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
返回 score 在 [min, max] 范围内的元素. 支持 -inf / +inf 表示无穷. 前缀 ( 表示开区间. Redis 6.2+ 可用 ZRANGE key min max BYSCORE 替代.
|
ZCOUNT
ZCOUNT key min max
统计 score 在 [min, max] 范围内的元素数量. O(logN).
ZREMRANGEBYRANK / ZREMRANGEBYSCORE
ZREMRANGEBYRANK key start stop / ZREMRANGEBYSCORE key min max
按排名/分值范围批量删除. 常用于维护固定容量的排行榜.
|
2.3 字典序查询(同分值场景)
当所有元素的 score 相同时, Sorted Set 退化为一个按字典序排列的有序集合. 此时可以用 LEX 系列命令做范围查询.
ZRANGEBYLEX
ZRANGEBYLEX key min max [LIMIT offset count]
按字典序范围查询. min/max 语法: [value 表示闭区间, (value 表示开区间, - 表示最小, + 表示最大.
|
应用: 自动补全
将所有候选词以 score=0 存入 ZSet, 用 ZRANGEBYLEX 做前缀范围查询. 比模糊匹配更高效, 且天然有序. 搜索框自动补全, 词典查找都可以这样实现.
2.4 集合运算
ZUNIONSTORE / ZINTERSTORE
ZUNIONSTORE dest numkeys key [key ...] [WEIGHTS weight ...] [AGGREGATE SUM|MIN|MAX]
对多个 ZSet 求并集/交集, 结果存入 dest. WEIGHTS 对不同 key 的 score 加权; AGGREGATE 决定相同 member 的 score 如何合并(默认 SUM).
|
ZUNIONSTORE + WEIGHTS = 评委打分系统 -- 多个评委(不同 ZSet)给同一组选手打分, 可以指定每个评委的权重, 最终合成综合分排名.
2.5 典型应用场景
| 场景 | score 代表什么 | 关键命令 |
|---|---|---|
| 排行榜 | 分数, 点赞数, 销量 | ZINCRBY + ZREVRANGE |
| 延迟队列 | 执行时间戳 | ZADD + ZRANGEBYSCORE 轮询 |
| 滑动窗口限流 | 请求时间戳 | ZADD + ZREMRANGEBYSCORE + ZCARD |
| Timeline/Feed | 发布时间戳 | ZADD + ZREVRANGEBYSCORE |
| 优先级队列 | 优先级数值 | ZADD + ZPOPMIN |
| 自动补全 | 全设为 0(利用字典序) | ZRANGEBYLEX |
| 多维度排序 | 加权综合分 | ZUNIONSTORE + WEIGHTS |
延迟队列示例
|
滑动窗口限流示例
|
滑动窗口 vs 固定窗口
第 02 篇的 String INCR + EXPIRE 实现的是固定窗口限流(整分钟/整秒重置). Sorted Set 实现的是滑动窗口--任意连续 60 秒内不超过 N 次, 更精确但内存消耗更大(每个请求都要记录). 两者各有适用场景.
3. Set vs Sorted Set 对比
| 维度 | Set | Sorted Set |
|---|---|---|
| 有序性 | 无序 | 按 score 有序 |
| 元素唯一性 | ✅ | ✅(member 唯一, score 可重复) |
| 查询复杂度 | SISMEMBER O(1) | ZSCORE O(1), ZRANK O(logN) |
| 范围查询 | ❌ 不支持 | ✅ 按排名/分值/字典序 |
| 集合运算 | SINTER/SUNION/SDIFF | ZINTERSTORE/ZUNIONSTORE(支持加权) |
| 随机操作 | SRANDMEMBER/SPOP | ZRANDMEMBER(Redis 6.2+) |
| 内存消耗 | 较小 | 较大(额外存 score + skiplist 索引) |
| 典型场景 | 去重, 标签, 黑名单 | 排行榜, 延迟队列, 限流 |
选择原则
问自己一个问题: "需要排序或范围查询吗?" 需要 → Sorted Set; 不需要 → Set 更轻量. 别为了"以防万一"选 ZSet--额外的 score 和 skiplist 结构意味着更多内存开销.
快速回顾
| 类型 | 核心命令 | 记忆要点 |
|---|---|---|
| Set | SADD / SREM |
增删元素, 自动去重 |
SISMEMBER |
O(1) 判断是否存在 | |
SINTER / SUNION / SDIFF |
交并差集运算 | |
SPOP / SRANDMEMBER |
随机弹出/查看(抽奖) | |
| Sorted Set | ZADD / ZINCRBY |
添加/原子加分 |
ZSCORE / ZRANK |
查分值/查排名 | |
ZRANGE / ZREVRANGE |
按排名范围取元素 | |
ZRANGEBYSCORE |
按分值范围取元素 | |
ZUNIONSTORE |
多集合加权合并 |
动手练习
- Set 标签: 为
user:A和user:B各建一个标签 Set(至少 4 个标签, 有部分重叠), 然后用 SINTER 找出共同标签, 用 SDIFF 找出 A 有但 B 没有的. - Set 抽奖: 向
lottery集合加入 10 个用户, 用 SPOP 抽出 3 个中奖者, 验证他们不在剩余集合中. - ZSet 排行榜: 创建一个游戏排行榜(至少 5 个玩家), 然后用 ZREVRANGE 取 Top 3; 用 ZINCRBY 给某个玩家加分并观察排名变化; 用 ZRANGEBYSCORE 查询 2000~4000 分的玩家; 用 ZRANK 查某个玩家的排名.
- ZSet 延迟队列: 用当前时间戳 + N 秒作为 score 添加 3 个任务, 然后用 ZRANGEBYSCORE 模拟"取出到期任务".
- ZSet 加权排序: 创建两个评分维度(如 quality 和 popularity), 用 ZUNIONSTORE + WEIGHTS 合成综合分, 对比不同权重下排名的变化.
- 思考题: 微博的"共同关注"功能用什么数据结构实现最合适? 如果要显示"你关注的人中有 N 人也关注了 TA", 命令链路是怎样的?