Set / Sorted Set —— 集合运算与排行榜的利器
Set 是一个无序、不重复的字符串集合。底层在元素数量少且都是整数时使用 intset 编码,否则使用 hashtable。
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+)批量判断多个元素。
# 用户标签系统
SADD user:1001:tags "golang" "redis" "docker" "k8s"
→ (integer) 4
# 重复添加无效
SADD user:1001:tags "redis" "mysql"
→ (integer) 1 ← 只有 mysql 是新增的
SISMEMBER user:1001:tags "redis" → (integer) 1
SISMEMBER user:1001:tags "java" → (integer) 0
SREM user:1001:tags "docker" → (integer) 1
SMEMBERS / SCARD
SMEMBERS key
SCARD key
SMEMBERS 返回集合的所有元素(无序)。SCARD 返回集合的元素数量(O(1))。
SMEMBERS user:1001:tags
1) "golang"
2) "redis"
3) "k8s"
4) "mysql"
SCARD user:1001:tags → (integer) 4
SMEMBERS 的性能注意
与 HGETALL 类似,当集合很大时 SMEMBERS 会一次返回全部元素。大集合应使用 SSCAN 渐进式遍历。
Set 最强大的能力是集合间运算——交集、并集、差集,且都是原子操作。
SINTER / SUNION / SDIFF
SINTER key [key ...]
SUNION key [key ...]
SDIFF key [key ...]
SINTER 交集(共同拥有的);SUNION 并集(合在一起的);SDIFF 差集(第一个有但其他没有的)。
# 两个用户的技术栈标签
SADD user:1001:tags "golang" "redis" "docker" "k8s"
SADD user:1002:tags "java" "redis" "mysql" "k8s"
# 交集:共同技能
SINTER user:1001:tags user:1002:tags
1) "redis"
2) "k8s"
# 并集:所有技能汇总
SUNION user:1001:tags user:1002:tags
1) "golang"
2) "redis"
3) "docker"
4) "k8s"
5) "java"
6) "mysql"
# 差集:1001 有但 1002 没有的
SDIFF user:1001:tags user:1002:tags
1) "golang"
2) "docker"
SINTERSTORE / SUNIONSTORE / SDIFFSTORE
SINTERSTORE destination key [key ...]
SUNIONSTORE destination key [key ...]
SDIFFSTORE destination key [key ...]
同上,但把结果存储到 destination 而非直接返回。适合结果集较大或需要后续复用的场景。
SRANDMEMBER
SRANDMEMBER key [count]
随机返回集合中的元素但不移除。count > 0:返回不重复的 count 个;count < 0:返回可能重复的 |count| 个。
SPOP
SPOP key [count]
随机弹出(返回并移除)一个或多个元素。抽奖场景的核心命令。
# 抽奖池
SADD lottery "user_A" "user_B" "user_C" "user_D" "user_E"
# 预览候选人(不移除)
SRANDMEMBER lottery 3
1) "user_C"
2) "user_A"
3) "user_E"
# 抽出中奖者(移除,不可重复中奖)
SPOP lottery 2
1) "user_B"
2) "user_D"
# 剩余参与者
SMEMBERS lottery
1) "user_A"
2) "user_C"
3) "user_E"
| 场景 | 实现方式 | 关键命令 |
|---|---|---|
| 标签系统 | 每个实体一个 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 |
Sorted Set(简称 ZSet)是 Set 的升级版——每个元素关联一个分值(Score),集合内的元素按 score 从小到大自动排序,且元素仍然唯一(但不同元素可以有相同 score)。
底层实现
Sorted Set 在元素少(<128 个且值短)时使用 ziplist/listpack 编码;元素多时使用 skiplist + hashtable 双重结构——skiplist 支持范围查询 O(logN),hashtable 支持 O(1) 按 member 查 score。两者协作让 ZSet 既能排序又能精确查找。
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)。
# 游戏排行榜
ZADD leaderboard 2500 "player_A" 3200 "player_B" 1800 "player_C" 4100 "player_D"
→ (integer) 4
ZSCORE leaderboard "player_B" → "3200"
ZRANK leaderboard "player_B" → (integer) 2 ← 正序第3(0-based)
ZREVRANK leaderboard "player_B" → (integer) 1 ← 逆序第2(D第1、B第2)
ZINCRBY
ZINCRBY key increment member
对指定 member 的 score 做原子增减。member 不存在时从 0 开始。排行榜加分的核心命令。
# player_A 得了 500 分
ZINCRBY leaderboard 500 "player_A" → "3000"
# 查看当前排名变化
ZREVRANK leaderboard "player_A" → (integer) 2 ← 从第4升到第3
ZREM / ZCARD
ZREM key member [member ...]
ZCARD key
ZREM 移除成员。ZCARD 返回集合总数(O(1))。
Sorted Set 的杀手锏——按排名或按分值范围高效查询。
ZRANGE / ZREVRANGE
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]
按排名(下标)返回元素。Redis 6.2+ 的 ZRANGE 统一了所有范围查询能力(通过 BYSCORE/BYLEX/REV 标志)。加 WITHSCORES 同时返回分值。
# Top 3(分值最高的前3名)
ZREVRANGE leaderboard 0 2 WITHSCORES
1) "player_D"
2) "4100"
3) "player_B"
4) "3200"
5) "player_A"
6) "3000"
# 所有人(正序:分值从低到高)
ZRANGE leaderboard 0 -1 WITHSCORES
1) "player_C"
2) "1800"
3) "player_A"
4) "3000"
5) "player_B"
6) "3200"
7) "player_D"
8) "4100"
ZRANGEBYSCORE / ZREVRANGEBYSCORE
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
返回 score 在 [min, max] 范围内的元素。支持 -inf / +inf 表示无穷。前缀 ( 表示开区间。Redis 6.2+ 可用 ZRANGE key min max BYSCORE 替代。
# 分值在 2000~3500 之间的玩家
ZRANGEBYSCORE leaderboard 2000 3500 WITHSCORES
1) "player_A"
2) "3000"
3) "player_B"
4) "3200"
# 开区间:大于 3000(不含3000)
ZRANGEBYSCORE leaderboard (3000 +inf WITHSCORES
1) "player_B"
2) "3200"
3) "player_D"
4) "4100"
# 配合 LIMIT 分页:跳过前1个,取2个
ZRANGEBYSCORE leaderboard -inf +inf LIMIT 1 2
1) "player_A"
2) "player_B"
ZCOUNT
ZCOUNT key min max
统计 score 在 [min, max] 范围内的元素数量。O(logN)。
ZREMRANGEBYRANK / ZREMRANGEBYSCORE
ZREMRANGEBYRANK key start stop
ZREMRANGEBYSCORE key min max
按排名/分值范围批量删除。常用于维护固定容量的排行榜。
# 只保留 Top 100,删除排名 100 以后的
ZREMRANGEBYRANK leaderboard 0 -101 ← 删除正序排名 0 到倒数第101
# 删除 7 天前的数据(用时间戳做 score)
ZREMRANGEBYSCORE events 0 1717200000
当所有元素的 score 相同时,Sorted Set 退化为一个按字典序排列的有序集合。此时可以用 LEX 系列命令做范围查询。
ZRANGEBYLEX
ZRANGEBYLEX key min max [LIMIT offset count]
按字典序范围查询。min/max 语法:[value 表示闭区间,(value 表示开区间,- 表示最小,+ 表示最大。
# 所有 score 设为 0,实现有序字典
ZADD cities 0 "beijing" 0 "chengdu" 0 "guangzhou" 0 "hangzhou" 0 "shanghai"
# 查找 b~h 之间(含首字母)
ZRANGEBYLEX cities "[b" "(i"
1) "beijing"
2) "chengdu"
3) "guangzhou"
4) "hangzhou"
# 自动补全:前缀匹配 "ch"
ZRANGEBYLEX cities "[ch" "[ch\xff"
1) "chengdu"
应用:自动补全
将所有候选词以 score=0 存入 ZSet,用 ZRANGEBYLEX 做前缀范围查询。比模糊匹配更高效,且天然有序。搜索框自动补全、词典查找都可以这样实现。
ZUNIONSTORE / ZINTERSTORE
ZUNIONSTORE dest numkeys key [key ...] [WEIGHTS w1 w2 ...] [AGGREGATE SUM|MIN|MAX]
ZINTERSTORE dest numkeys key [key ...] [WEIGHTS ...] [AGGREGATE ...]
对多个 ZSet 求并集/交集,结果存入 dest。WEIGHTS 对不同 key 的 score 加权;AGGREGATE 决定相同 member 的 score 如何合并(默认 SUM)。
# 多维度评分合并
ZADD score:quality 80 "article_A" 90 "article_B" 70 "article_C"
ZADD score:popularity 1000 "article_A" 500 "article_B" 2000 "article_C"
# 加权综合评分:质量权重 0.6,人气权重 0.4
ZUNIONSTORE score:final 2 score:quality score:popularity WEIGHTS 0.6 0.4
→ (integer) 3
ZREVRANGE score:final 0 -1 WITHSCORES
1) "article_C"
2) "842" ← 70*0.6 + 2000*0.4 = 42 + 800
3) "article_A"
4) "448" ← 80*0.6 + 1000*0.4 = 48 + 400
5) "article_B"
6) "254" ← 90*0.6 + 500*0.4 = 54 + 200
| 场景 | score 代表什么 | 关键命令 |
|---|---|---|
| 排行榜 | 分数、点赞数、销量 | ZINCRBY + ZREVRANGE |
| 延迟队列 | 执行时间戳 | ZADD + ZRANGEBYSCORE 轮询 |
| 滑动窗口限流 | 请求时间戳 | ZADD + ZREMRANGEBYSCORE + ZCARD |
| Timeline/Feed | 发布时间戳 | ZADD + ZREVRANGEBYSCORE |
| 优先级队列 | 优先级数值 | ZADD + ZPOPMIN |
| 自动补全 | 全设为 0(利用字典序) | ZRANGEBYLEX |
| 多维度排序 | 加权综合分 | ZUNIONSTORE + WEIGHTS |
# 生产者:添加延迟任务(score = 执行时间戳)
ZADD delay_queue 1717200060 "send_email:user_42"
ZADD delay_queue 1717200120 "notify:order_1001"
# 消费者:每秒轮询当前时刻到期的任务
ZRANGEBYSCORE delay_queue 0 1717200065 ← 当前时间戳
1) "send_email:user_42" ← 这条已到期
# 取出后删除(原子操作用 ZPOPMIN 或 Lua 脚本)
ZPOPMIN delay_queue
1) "send_email:user_42"
2) "1717200060"
# 每个请求记录时间戳(member 用唯一ID避免重复)
ZADD rate:user_123 1717200001 "req_uuid_1"
ZADD rate:user_123 1717200002 "req_uuid_2"
...
# 清除窗口外的旧记录(保留最近 60 秒)
ZREMRANGEBYSCORE rate:user_123 0 1717199940
# 统计窗口内的请求数
ZCARD rate:user_123 → 当前窗口内请求数
# 若超过阈值则拒绝
# if ZCARD > 100 → reject
滑动窗口 vs 固定窗口
第 02 篇的 String INCR + EXPIRE 实现的是固定窗口限流(整分钟/整秒重置)。Sorted Set 实现的是滑动窗口——任意连续 60 秒内不超过 N 次,更精确但内存消耗更大(每个请求都要记录)。两者各有适用场景。
| 维度 | 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 |
多集合加权合并 |
user:A 和 user:B 各建一个标签 Set(至少 4 个标签,有部分重叠),然后用 SINTER 找出共同标签,用 SDIFF 找出 A 有但 B 没有的。lottery 集合加入 10 个用户,用 SPOP 抽出 3 个中奖者,验证他们不在剩余集合中。