文章目录
-
-
- [1. 为什么不能只用跳表(SkipList)?](#1. 为什么不能只用跳表(SkipList)?)
- [2. 为什么不能只用字典(Dict)?](#2. 为什么不能只用字典(Dict)?)
- [3. 联手的威力:1 + 1 > 2](#3. 联手的威力:1 + 1 > 2)
- [4. 一个关键问题:会浪费两倍内存吗?](#4. 一个关键问题:会浪费两倍内存吗?)
- [5. 补充:字典的维护(参考图片信息)](#5. 补充:字典的维护(参考图片信息))
- 总结
-
这是一个非常精妙的设计。简单来说,Redis 在 zset 大量数据时同时采用 跳表(SkipList) 和 字典(Dict) ,是为了实现 "全方位的性能无死角" 。
如果只用其中一个,zset 的某些常用操作就会变得非常慢。我们将它们这种组合称为"强强联手":
1. 为什么不能只用跳表(SkipList)?
跳表的主要优势在于有序性 和区间查询。
- 它的强项: 它可以以 O ( log N ) O(\log N) O(logN) 的时间复杂度找到某个分值(Score)的起始位置,然后进行范围遍历(比如
ZRANGE)。 - 它的弱点: 如果你想知道"成员
user_1的分数是多少?"(ZSCORE命令),跳表必须从顶层向下层层查找,复杂度是 O ( log N ) O(\log N) O(logN)。
虽然 O ( log N ) O(\log N) O(logN) 已经很快了,但 Redis 追求的是极致。
2. 为什么不能只用字典(Dict)?
字典(哈希表)的优势在于极速定位。
- 它的强项: 给它一个成员名,它能以 O ( 1 ) O(1) O(1) 的时间告诉你它的分数或是否存在。
- 它的弱点: 字典是无序 的。如果你想执行"获取分数前 10 名"或者"获取分数在 100 到 200 之间的人",字典只能全表扫描并排序,复杂度高达 O ( N log N ) O(N \log N) O(NlogN)。
3. 联手的威力:1 + 1 > 2
Redis 开发者将这两者结合起来,形成了一个复合结构:
| 操作类型 | 对应命令 | 使用的结构 | 复杂度 |
|---|---|---|---|
| 按成员找分数 | ZSCORE |
Dict | O ( 1 ) O(1) O(1) |
| 判断成员是否存在 | ZISMEMBER |
Dict | O ( 1 ) O(1) O(1) |
| 获取排名区间 | ZRANGE |
SkipList | O ( log N + M ) O(\log N + M) O(logN+M) |
| 按分数段查询 | ZRANGEBYSCORE |
SkipList | O ( log N + M ) O(\log N + M) O(logN+M) |
4. 一个关键问题:会浪费两倍内存吗?
你可能会担心:存两份数据,内存不就爆了吗?
并不会。
- 指针共享: 实际上,成员字符串(Member String)在内存中只存了一份。
- 跳表节点里的指针和字典里的指针,都指向同一个字符串对象的内存地址。
- 虽然跳表和字典本身会占用一些额外的索引空间(指针),但相比于实际存储的大量数据,这点开销换取性能的全方位提升是非常划算的。
5. 补充:字典的维护(参考图片信息)
正如你提供的图片 所示,zset 里的这个 Dict 也会经历 渐进式 rehash。
- 当
zset不断增大导致 Dict 需要扩容时,Redis 不会一次性搬迁,而是分多次完成。 - 在 rehash 过程中,
zset的新增操作会直接写入新表ht[1],这确保了 rehash 期间zset的ZSCORE等操作依然能保持高效,且不会阻塞主线程。
总结
zset 选这种组合,是因为:
- 用 Dict 保证了根据成员名查分数的 O ( 1 ) O(1) O(1) 性能。
- 用 SkipList 保证了范围查询和排序的 O ( log N ) O(\log N) O(logN) 性能。
- 通过 指针共享 解决了内存浪费问题。
这种"空间换时间"且"多索引指向同一数据"的思想,也是很多大型数据库(如 MySQL 的聚簇索引和二级索引)的核心设计逻辑。