Redis数据结构
1.Set
Set(集合)底层数据结构:intset / hashtable。查询的时间复杂度为O(1)。
Redis 会根据数据大小和类型自动切换编码:
| 编码 | 数据结构 | 触发条件 |
|---|---|---|
intset | 整数集合(紧凑数组) | 所有元素都是整数,且数量 ≤ 512 |
hashtable | 哈希表 | 元素包含字符串 / 数量超过限制 |
一旦从 intset 升级为 hashtable,就不会降级回来。
✅intset(整数集合)结构如下:使用二分查找判断是否存在、内存紧凑,适合小整数集合。📌 时间复杂度:O(log n)
typedef struct intset {uint32_t encoding; // 编码格式:16/32/64位uint32_t length; // 当前元素个数int8_t contents[]; // 存储整数的有序数组(节省内存)
} intset;
✅ hashtable(哈希表):实际是 Redis 的 dict 结构、key 是 set 元素,value 为 NULL。
查找时间复杂度:O(1)
// 简化版
dict {dictEntry **table; // 哈希桶数组int size; // 容量int used; // 已用数量
}
当 set 中有非整数或数量太多时,自动升级为 hashtable
2.ZSet
ZSet(有序集合)底层数据结构为:ziplist/listpack 或 skiplist + dict。查询的时间复杂度为O(log N)。
底层编码方式(两种):
| 编码 | 数据结构 | 触发条件 |
|---|---|---|
ziplist(旧)或 listpack(Redis 7.0+) | 压缩列表 | 元素少(默认 ≤ 128),且 member 和 score 较小 |
skiplist + dict | 跳表 + 字典 | 超出上述限制时升级 |
💡 在新版 Redis 中,
ziplist已被更安全高效的listpack替代。
ZSet 从 listpack 转成 skiplist的时机,满足以下条件的任何一种:
- 元素个数 >
zset-max-listpack-entries(默认 128)。 - 任一 member 或 score 的长度 >
zset-max-listpack-value(默认 64 字节)。
核心结构之双索引设计:
typedef struct zset {dict *dict; // member → score 映射(O(1) 查 score)zskiplist *zsl; // score 排序链表(支持范围查询)
} zset;
| 结构 | 作用 |
|---|---|
dict | 快速查找某个 member 的 score(用于 ZSCORE) |
zskiplist | 按 score 排序,支持 ZRANGE, ZRANK, ZREVRANGE 等 |
2.1.ziplist 的整体结构图解
+------------------+--------+-------------------+--------+------------------+
| zlbytes (4B) | zltail (4B) | zllen (2B) | ... entry ... | end (1B) |
+------------------+--------+-------------------+--------+------------------+
各字段含义:
| 字段 | 大小 | 说明 |
|---|---|---|
zlbytes | 4 字节 | 整个 ziplist 占用的总字节数 |
zltail | 4 字节 | 最后一个 entry 的偏移量(便于从尾部遍历) |
zllen | 2 字节 | entry 的数量(注意:最大 65535,所以不能精确统计超过此数) |
entry... | 变长 | 实际存储的数据项(多个 entry 连续排列) |
end | 1 字节 | 特殊标记 0xFF,表示 ziplist 结束 |
所有数据都在一块连续内存中。
ziplist 中的每个元素称为一个 entry,其结构如下:
[prev_length] [encoding] [data]
1. prev_length:前一个 entry 的长度(用于反向遍历)
- 如果前一个 entry 长度 < 254 字节 → 用 1 字节存储
- 否则 → 用 5 字节(第一个字节是
0xFE,后面 4 字节是真实长度)
👉 这样可以从尾部向前遍历(类似双向链表)
2. encoding:当前 entry 的编码方式
用来标识后面 data 的类型和长度。
常见编码:
| 编码值(十六进制) | 含义 |
|---|---|
0xxx xxxx | 8/16/32 位整数(如 00 表示 int8) |
1100 0000 ~ 1111 1110 | 字符串,高位表示长度编码方式 |
1111 1111 | 特殊用途,如 end 标记 |
3. data:实际存储的数据
- 如果是字符串:直接存原始内容
- 如果是整数:按 encoding 规则存二进制值(不转成字符串)
2.2.listpack
由于 ziplist 存在严重的 连锁更新问题,Redis 在 7.0 版本中引入了替代品:listpack。listpack 的改进:
| 特性 | ziplist | listpack |
|---|---|---|
| entry 大小记录 | 在 前一个 entry 中 | 在 当前 entry 头部 |
| 是否支持独立修改 | ❌ 否(影响后续) | ✅ 是(不影响其他 entry) |
| 内存重分配风险 | 高(连锁更新) | 极低 |
| 实现复杂度 | 高 | 更简单安全 |
| 是否已弃用 | ✅ 是(Redis 7.0+) | ✅ 新默认编码 |
listpack 结构更清晰:
+--------+--------+--------+------------------+--------+
| total_bytes | count | ... entry ... | end (0xFF) |
+--------+--------+--------+------------------+--------+
每个 entry 自带长度信息,不再依赖前一个节点。
2.3.跳表+哈希表dict
在 Redis 的 zset(有序集合)中,当数据量超过压缩列表的阈值(元素数量过多或元素值过长)时,底层存储结构会转为 skiplist + dict 的组合结构。这种组合兼顾了有序性查询和快速的成员 - 分值映射,是 Redis 高效实现有序集合的核心设计。
skiplist + dict 的角色分工。zset 的 skiplist + dict 结构中,两个组件分别承担不同职责:
- 跳表(skiplist):负责维护元素的有序性,支持按分值(score)范围查询、排名计算等操作。
- 字典(dict,哈希表):负责维护成员(value)到分值(score)的映射,支持通过成员快速查找对应的分值,以及判断成员是否存在。
两者存储的是同一批 zset 元素((value, score) 对),但组织方式不同,形成互补:跳表提供有序性和范围操作能力,字典提供 O (1) 级别的成员查询能力。
跳表(skiplist)的结构与作用。跳表是一种有序数据结构,通过在每个节点中维护多个指向其他节点的指针,实现快速查找、插入和删除。Redis 中的跳表(zskiplist)结构如下:
跳表(zskiplistNode)每个节点代表 zset 中的一个元素,包含:
value:成员值(字符串)。score:分值(双精度浮点数),跳表按此值从小到大排序。backward:指向当前节点的前一个节点(用于从后向前遍历)。level:节点的层数(随机生成,决定该节点有多少个向前的指针)。forward[]:每层的向前指针数组,forward[i]指向当前节点在第i层的下一个节点。
跳表的整体结构包含:
header:头节点(不存储实际数据,作为查询的起点)。tail:尾节点(指向最后一个元素)。length:跳表中的节点总数(即zset的元素数量)。level:当前跳表中最高节点的层数(方便查询时从最高层开始)。
跳表的作用:
- 支持按
score范围查询(如ZRANGEBYSCORE)、按排名查询(如ZRANGE)、计算元素排名(如ZRANK)等有序操作,时间复杂度平均为 O (log N)。 - 所有节点按
score升序排列,若score相同,则按value的字典序排序。
字典(dict)的结构与作用:字典(哈希表)是 Redis 中用于快速映射的结构,zset 中的字典以 value 为键,score 为值,即 dict[value] = score。
字典的作用:
- 支持通过
value快速查找对应的score(如ZSCORE命令),时间复杂度为 O (1)。 - 快速判断
value是否存在于zset中(如ZISMEMBER命令)。 - 插入或删除元素时,通过字典快速定位
value对应的score,避免遍历跳表。
skiplist + dict 的协同工作,zset 中的 skiplist 和 dict 存储的是同一批元素,两者保持强一致性:
- 插入元素:当新增
(value, score)时,既要在跳表中插入对应的节点(按score排序),也要在字典中添加value → score的映射。 - 删除元素:删除时,需同时从跳表中移除节点和从字典中删除键值对。
- 更新分值:若通过
ZADD命令更新某个value的score,会先通过字典找到旧score,然后从跳表中删除旧节点,再插入新score对应的节点,并更新字典中的映射。
这种组合的优势:
- 跳表的优势是有序性和范围操作,但通过
value查找score时效率较低(需遍历)。 - 字典的优势是 O (1) 级别的
value → score映射,但无法维护有序性和支持范围查询。
两者结合后,zset 既能高效处理有序相关的操作(如排名、范围查询),又能快速通过成员定位分值,兼顾了性能和功能需求。
总结,Redis 的 zset 在使用 skiplist + dict 结构时:
- 跳表负责维护元素的有序性,支持按分值 / 排名的范围操作。
- 字典负责成员到分值的快速映射,支持 O (1) 级别的查询和存在性判断。
- 两者存储相同的元素,操作时保持同步,共同实现了有序集合的高效功能。
