# 29. Redis 中的 Sorted Set 排序是如何实现的?如何优化其性能?
# 标准答案
Redis 的 Sorted Set(有序集合) 采用 跳表(SkipList)+ 哈希表(HashTable) 进行排序和存储。
- 跳表(SkipList) 提供 O(logN) 的范围查询、排名查询、排序等操作。
- 哈希表(HashTable) 通过 Key 快速定位到对应的分值(score)。
- 数据按 score 从小到大自动排序,插入、删除、排名查询均保持高效。
优化方式:
- 减少跳表层数(优化插入和查询)
- 使用 Pipeline 批量操作(减少网络交互)
- 合理设计 score 值(避免大量相同 score)
- 使用 ZRANGEBYLEX、ZSCAN 代替 ZRANGE 扫描
# 答案解析
# 1️⃣ Sorted Set 的数据结构
Redis 的 Sorted Set(有序集合,ZSet) 是 一个排序的数据集合,每个元素包含:
- Value(成员)
- Score(分值)
Sorted Set 自动按照 score 升序排列,可高效执行 排名、范围查询、Top-N 查询 等操作。
Redis 采用 跳表(SkipList)+ 哈希表(HashTable) 存储数据:
- 哈希表 提供 O(1) 访问,快速定位成员的 score。
- 跳表 提供 O(logN) 查询,支持 排序、范围查找、排名计算。
# 2️⃣ 跳表(SkipList)的实现
# 🔹(1)什么是跳表?
跳表是一种 有序链表的多层索引结构,用于高效查询、插入、删除。
操作 | 跳表复杂度 |
---|---|
插入 | O(logN) |
删除 | O(logN) |
查询 | O(logN) |
排序 | 天然有序(O(1)) |
🔹 跳表的基本结构:
Level 3: ----> 10 ----> 30 ----> 50 ----> 70 ---->
Level 2: ----> 10 ----> 30 ----> 50 ----> 70 ---->
Level 1: ----> 10 ----> 20 ----> 30 ----> 40 ----> 50 ----> 60 ----> 70 ---->
2
3
- 底层是一个普通的有序链表
- 每一层都是上一层的子集,用于加速查询
- 查询时,从高层向下逐步定位
🔹 查询过程(查找 score=45):
- 从顶层(Level 3)开始,找到
30
(比 45 小)。 - 向右查找,找到
50
(比 45 大),回退到30
位置。 - 向下移动到 Level 2,继续向右找,仍然是
50
,再向下移动。 - 最终在 Level 1 找到 40 → 50 之间,确定 45 不存在。
跳表的查询 类似二分查找,复杂度为 O(logN)。
# 3️⃣ HashTable 的作用
哈希表(HashTable)用于:
- 快速找到成员的分值(O(1))
- 跳表按 score 排序,哈希表加速查找
📌 示例:
struct zset {
dict *dict; // 哈希表,O(1) 查找
zskiplist *zsl; // 跳表,O(logN) 排序
};
2
3
4
优势:
- 跳表 O(logN) 维护排序
- 哈希表 O(1) 快速查找元素
- 范围查询(Top-N)比红黑树更快
# 4️⃣ Sorted Set 操作的时间复杂度
操作 | 命令 | 复杂度 |
---|---|---|
添加元素 | ZADD key score member | O(logN) |
删除元素 | ZREM key member | O(logN) |
获取排名 | ZRANK key member | O(logN) |
按范围查询 | ZRANGE key start stop | O(logN + M) |
取 Top-N | ZREVRANGE key 0 N-1 | O(logN + N) |
# 5️⃣ 如何优化 Sorted Set 性能
Sorted Set 适用于 排行榜、积分榜、Top-N 查询,但大规模数据会影响性能,优化策略包括:
# 🔹(1)减少跳表层数
跳表默认最大 32 层,实际使用时,元素 少于 100 万时,一般 5~7 层已足够。
- 减少层数 → 降低插入开销
- 增大
P
(层级概率) → 减少层高
CONFIG SET zset-max-level 8
# 🔹(2)使用 Pipeline 批量操作
Redis 单条命令 O(logN),但网络交互会增加延迟,使用 Pipeline 批量提交,减少 RTT:
pipeline.zadd("ranking", 100, "user1");
pipeline.zadd("ranking", 90, "user2");
pipeline.zadd("ranking", 80, "user3");
pipeline.sync(); // 批量执行
2
3
4
✅ 减少 TCP 交互,提高吞吐量
# 🔹(3)合理设计 score
- 避免相同 score(影响排名稳定性)
- 使用
时间戳 + 偏移量
避免冲突
ZADD leaderboard 1699999999.0001 "userA"
# 🔹(4)使用 ZRANGEBYLEX 代替 ZRANGE
当 score 相同时,ZRANGE
可能有性能问题,可用 ZRANGEBYLEX
进行字典序查询:
ZRANGEBYLEX myset [a [c
# 🔹(5)使用 ZSCAN 代替全量遍历
避免 ZRANGE key 0 -1
全量遍历,使用 ZSCAN
进行渐进式扫描:
ZSCAN myzset 0 COUNT 10
✅ 降低单次操作开销,减少阻塞
# 深入追问
🔹 Redis 为什么用跳表,而不是红黑树(RB Tree)?
🔹 在百万级 Sorted Set 下,如何优化排名查询?
🔹 Sorted Set 在分布式 Redis Cluster 下如何扩展?
# 相关面试题
🔹 Redis 跳表的原理,为什么不用红黑树?
🔹 Redis 如何优化排行榜(Top-N 查询)?
🔹 Redis Sorted Set 如何避免大 Key 影响性能?
# 总结
Redis Sorted Set 采用 SkipList + HashTable 组合,提供高效排序、查询、排名功能。优化方式包括:
- 减少跳表层数
- 使用 Pipeline 批量操作
- 优化 score 设计
- 使用
ZRANGEBYLEX
和ZSCAN
避免全量查询 - 分片存储大规模 Sorted Set
在 排行榜、积分榜、推荐系统 等场景下,合理优化可以显著提升性能!