在 Redis 的五种核心数据结构中,Sorted Set(有序集合,简称 ZSET) 是唯一同时具备"集合去重"与"按分数排序"能力的类型。它不仅是实现实时排行榜的首选方案,还在延迟队列、带权重任务调度、滑动窗口限流等场景中大放异彩。
一、Sorted Set 的底层数据结构演进
Redis 对 Sorted Set 的底层实现经历了从 ziplist 到 skiplist + dict 的优化路径,其选择策略由配置参数动态决定:
| 条件 | 底层结构 |
|---|---|
元素数量 ≤ zset-max-ziplist-entries(默认 128)且每个元素的 score 可用整数表示且所有 member 长度 ≤ zset-max-ziplist-value(默认 64 字节) |
ziplist(紧凑内存布局,节省空间) |
| 不满足上述任一条件 | skiplist + dict(跳表支持 O(log N) 插入/查询,dict 提供 O(1) 成员存在性检查) |
注意:自 Redis 7.0 起,
ziplist已被更高效的 listpack 替代,但逻辑判断逻辑不变。
内存与性能权衡
- ziplist/listpack:内存友好,但修改操作需重建整个结构,适合小规模静态数据。
- skiplist :支持高效范围查询(如
ZRANGE),适用于高频更新的大规模排行榜。
| 特性 | ziplist |
listpack |
|---|---|---|
| 长度存储位置 | 每个 entry 的前一个 entry 尾部存其长度(backward length) | 每个 entry 自己头部存自己的总长度 |
| 级联更新 | ✅ 存在(修改 entry 可能触发整体重排) | ❌ 不存在(entry 自包含,互不影响) |
| 元素最大长度 | 理论支持大值,但实际受限于级联更新风险 | 明确限制:单个 entry ≤ 2^16 - 1 = 65535 字节 |
| 结束标记 | 以 0xFF 字节结尾 |
以 4 字节 total-bytes + 0x00 结尾(可快速定位尾部) |
| 内存布局 | <zlbytes><zltail><zllen><entry1><entry2>...<zlend> |
<total_bytes><entry1><entry2>...<num_elements><0x00> |
二、核心命令详解
以下为 Sorted Set 最常用命令及其典型用法:
| 命令 | 时间复杂度 | 用途说明 | 示例 |
|---|---|---|---|
| `ZADD key [NX | XX] [GT | LT] score member [score member ...]` | O(log N) |
| `ZRANGE key start stop [BYSCORE | BYLEX] [REV] [WITHSCORES]` | O(log N + M) | 按排名或分数范围获取成员 |
ZREVRANK key member |
O(log N) | 获取成员倒序排名(Top 1 为 0) | ZREVRANK leaderboard "user:1001" |
| `ZUNIONSTORE dest numkeys key [key ...] [WEIGHTS w1 w2 ...] [AGGREGATE SUM | MIN | MAX]` | O(N)+O(M log M) |
ZINTER key [key ...] [WEIGHTS ...] [AGGREGATE ...] (Redis 6.2+) |
O(N*K)+O(M log M) | 交集计算(如共同关注用户打分) | --- |
| `ZMPOP numkeys key [key ...] MIN | MAX count` (Redis 7.0+) | O(K*log N) | 原子弹出最小/最大元素(替代 ZRANGE + ZREM 组合) |
| `BZMPOP timeout numkeys key [key ...] MIN | MAX count` (Redis 7.0+) | O(K*log N) | 阻塞版 ZMPOP,适用于延迟队列消费者 |
最佳实践 :优先使用
ZMPOP/BZMPOP替代旧式ZRANGE + ZREM,避免竞态条件。
三、典型应用场景
场景 1:实时游戏排行榜(Top N)
-
需求:每秒万级玩家分数更新,毫秒级返回 Top 100。
-
实现 :
bashZADD game_rank <score> <player_id> ZREVRANGE game_rank 0 99 WITHSCORES -
优势:天然去重、自动排序、支持分页。
场景 2:延迟队列(Delay Queue)
-
设计:以执行时间戳为 score,任务 ID 为 member。
-
消费 :
bash# 非阻塞轮询 ZRANGEBYSCORE delay_queue -inf <current_timestamp> LIMIT 0 10 # 或使用 Redis 7.0+ BZMPOP 5 1 delay_queue MIN 1
场景 3:带权重的任务调度
- 多个任务池按优先级(score = priority * time_factor)合并,用
ZUNION动态生成调度序列。
四、性能分析与调优建议
常见陷阱
- 大 Key 风险 :单个 ZSET 超过百万成员会导致
ZRANGE阻塞主线程。 - 内存膨胀:skiplist 每个节点平均占用约 32 字节(含指针开销)。
调优策略
| 问题 | 解决方案 |
|---|---|
| 排行榜过大 | 分片(如按地域/时间段拆分多个 ZSET) |
| 高频更新导致 CPU 飙升 | 使用 pipeline 批量写入;避免频繁 ZREMRANGEBYRANK |
| 内存不足 | 设置 maxmemory-policy allkeys-lru;监控 INFO memory 中 used_memory_dataset |
五、数据流示例:实时排行榜架构

六、高频面试题
-
Q:如何用 Sorted Set 实现一个 Top N 实时排行榜?
A :使用ZADD更新用户分数,ZREVRANGE key 0 N-1 WITHSCORES获取前 N 名。注意避免大 Key,可分片处理。 -
Q:ZSET 底层何时从 ziplist 切换到 skiplist?
A :当元素数量 >zset-max-ziplist-entries(默认 128)或任一 member 长度 >zset-max-ziplist-value(默认 64 字节)时切换。 -
Q:
ZUNION和ZINTER的时间复杂度为何较高?
A:需遍历所有输入集合,合并后排序,复杂度约为 O(N*K + M log M),其中 N 为平均集合大小,K 为集合数量,M 为结果集大小。 -
Q:Redis 7.0 的
BZMPOP相比旧方案有何优势?
A :原子性弹出最小/最大元素,避免ZRANGE + ZREM的竞态风险,且支持阻塞等待,简化延迟队列消费者逻辑。 -
Q:如何防止 Sorted Set 内存无限增长?
A :定期使用ZREMRANGEBYRANK清理尾部数据(如只保留 Top 10 万),或采用 TTL + 外部归档策略。