# 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 ---->
1
2
3
  • 底层是一个普通的有序链表
  • 每一层都是上一层的子集,用于加速查询
  • 查询时,从高层向下逐步定位

🔹 查询过程(查找 score=45):

  1. 从顶层(Level 3)开始,找到 30(比 45 小)。
  2. 向右查找,找到 50(比 45 大),回退到 30 位置。
  3. 向下移动到 Level 2,继续向右找,仍然是 50,再向下移动。
  4. 最终在 Level 1 找到 40 → 50 之间,确定 45 不存在

跳表的查询 类似二分查找,复杂度为 O(logN)

# 3️⃣ HashTable 的作用

哈希表(HashTable)用于:

  • 快速找到成员的分值(O(1))
  • 跳表按 score 排序,哈希表加速查找

📌 示例

struct zset {
    dict *dict;      // 哈希表,O(1) 查找
    zskiplist *zsl;  // 跳表,O(logN) 排序
};
1
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
1

# 🔹(2)使用 Pipeline 批量操作

Redis 单条命令 O(logN),但网络交互会增加延迟,使用 Pipeline 批量提交,减少 RTT:

pipeline.zadd("ranking", 100, "user1");
pipeline.zadd("ranking", 90, "user2");
pipeline.zadd("ranking", 80, "user3");
pipeline.sync();  // 批量执行
1
2
3
4

减少 TCP 交互,提高吞吐量

# 🔹(3)合理设计 score

  • 避免相同 score(影响排名稳定性)
  • 使用 时间戳 + 偏移量 避免冲突
ZADD leaderboard 1699999999.0001 "userA"
1

# 🔹(4)使用 ZRANGEBYLEX 代替 ZRANGE

当 score 相同时,ZRANGE 可能有性能问题,可用 ZRANGEBYLEX 进行字典序查询:

ZRANGEBYLEX myset [a [c
1

# 🔹(5)使用 ZSCAN 代替全量遍历

避免 ZRANGE key 0 -1 全量遍历,使用 ZSCAN 进行渐进式扫描:

ZSCAN myzset 0 COUNT 10
1

降低单次操作开销,减少阻塞

# 深入追问

🔹 Redis 为什么用跳表,而不是红黑树(RB Tree)?
🔹 在百万级 Sorted Set 下,如何优化排名查询?
🔹 Sorted Set 在分布式 Redis Cluster 下如何扩展?

# 相关面试题

🔹 Redis 跳表的原理,为什么不用红黑树?
🔹 Redis 如何优化排行榜(Top-N 查询)?
🔹 Redis Sorted Set 如何避免大 Key 影响性能?

# 总结

Redis Sorted Set 采用 SkipList + HashTable 组合,提供高效排序、查询、排名功能。优化方式包括:

  1. 减少跳表层数
  2. 使用 Pipeline 批量操作
  3. 优化 score 设计
  4. 使用 ZRANGEBYLEXZSCAN 避免全量查询
  5. 分片存储大规模 Sorted Set

排行榜、积分榜、推荐系统 等场景下,合理优化可以显著提升性能!