Files
autumn-recruitment/03.Redis/core/Redis 五大核心数据结构.md
T

149 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [redis, data-structures, sds, quicklist, skiplist]
create time: 2026-08-08 18:43
update time: 2026-08-08 18:43
---
# Redis 五大核心数据结构
## 概述
Redis 的性能基石在于其精心设计的底层数据结构。Redis 的每个值都有多种编码(encoding),会根据数据规模和内容自动切换,在内存占用和操作效率之间寻找最优解。理解这些编码的切换策略,是面试中的高频考点,也是性能调优的关键前提。
## 核心原理
### String — SDS(Simple Dynamic String)
Redis 的 String 类型不是直接用 C 字符串,而是用 SDS(Simple Dynamic String)结构。SDS 的定义如下:
```c
struct sdshdr {
int len; // buf 中已使用的字节数
int free; // buf 中未使用的字节数
unsigned char flags; // 低3位标记类型:SDS_TYPE_5/8/10/16/32
char buf[]; // 可变长度字符数组,以 \0 结尾
};
```
> [!NOTE]
> SDS 有 5 种格式(SDS_TYPE_5 到 SDS_TYPE_32),通过 `flags` 字段的低 3 位区分。SDS_TYPE_5 用于短字符串(len < 32),直接内嵌在 dictEntry 中而不分配独立对象。
**为什么不用 C 字符串?**
| 问题 | C 字符串 | SDS |
|------|---------|-----|
| O(N) 获取长度 | strlen 遍历计数 | len 字段 O(1) |
| 缓冲区溢出风险 | strcpy/strcat 不安全 | 分配时检查空间 |
| 修改需重新分配 | 每次可能 realloc | 惰性空间释放(free 保留) |
| 二进制安全 | 遇 \0 截断 | 记录 len,可存任意二进制数据 |
**innative 优化**:从 Redis 7.0 开始,小字符串使用 SDS_TYPE_5 直接存储在 dictEntry 的键槽中,完全避免额外的堆分配,减少内存碎片和 malloc 开销。
### List — quicklist
Redis 3.2 之前用 ziplist + linkedlist 实现 List,3.2 之后统一为 **quicklist**。
quicklist 是一个双向链表,每个节点(quicklistNode)是一个 ziplist 或 listpack(Redis 7.0+)。通过 `list-max-ziplist-size` 控制每个节点的压缩列表大小:
| list-max-ziplist-size 值 | 含义 |
|--------------------------|------|
| 正数 N | 该节点最多 N 个元素 |
| -1 | 每节点不限(仅受 memory 约束) |
| -2 | 每节点 <= 8KB |
| -3 | 每节点 <= 4KB |
| -4 | 每节点 <= 2KB |
| -5 | 每节点 <= 1KB(默认) |
```go
// Go 伪代码示意 quicklist 的结构
type quicklist struct {
head *quicklistNode
tail *quicklistNode
count int64 // 总元素数
zipsize int // list-max-ziplist-size 全局配置
}
type quicklistNode struct {
prev *quicklistNode
next *quicklistNode
ptr unsafe.Pointer // 指向 ziplist/listpack
sz uint32 // 字节大小
}
```
### Hash — zipmap / hashtable 切换
Hash 有两种编码,根据数据和元素数量自动切换:
- **zipmap**(Redis 4.0 之前)→ 当元素数量 >= 512 且最大 value 长度 < 64 字节时使用 hashtable
- **ziplist**(Redis 4.0 ~ 5.x)→ 元素数量 <= 512 且所有 value 长度 < 64 字节
- **hashtable**(Redis 4.0+ 默认)→ 只要有一个 value > 64 字节 或 元素数 > 512 就切换到 hashtables
> [!WARNING]
> Redis 4.0+ 废弃了 ziplist 作为 Hash 编码,默认始终使用 hashtable。这是因为 ziplist 插入删除的均摊代价在大数据量下反而更高。ziplist 编码目前仅在 ZSet 场景中保留。
### Set — intset → hashtable 切换
Set 编码切换逻辑非常直观:
```mermaid
graph TD
A["Set 创建"] --> B{"所有元素都是整数?"}
B -->|是| C["intset 编码"]
B -->|否| D["hashtable 编码"]
C --> E{"新加入的元素仍是整数?"}
E -->|是| F["继续 intset"]
E -->|否| G["转换为 hashtable"]
D --> H["维持 hashtable"]
```
- **intset**:有序整数集合,紧凑排列在连续内存中。支持 int16_t、int32_t、int64_t 三种格式,升级时 realloc 整个结构(不可降级)。
- **hashtable**:通用哈希表,任何类型的成员都能存储。
### ZSet — skiplist + ziplist / quicklist 切换
ZSet 是最复杂的结构,组合了两层编码:
- **ziplist**(Redis 5.0 之前)/ **quicklist**(Redis 5.0+):当元素少且值小时用压缩列表存储
- **skiplist + hashtable**:标准模式,跳表按 score 排序,hashtable 按 member 做 O(1) 查找
> [!TIP]
> 面试常考:跳表的平均查找复杂度 O(log N),最坏 O(N);Redis 跳表通过限制随机层级上限(maxlevel=32)和控制概率 p=0.25,保证最坏情况也在可接受范围。跳表节点包含:score(分数)、member(成员)、back(后退指针)、forward(多层前进指针数组)。
## 代码示例
以下展示如何查看 Redis 对象的内部编码:
```bash
# Redis CLI 中查看键的内部编码
127.0.0.1:6379> OBJECT ENCODED mykey
"ziplist" # 或 "skiplist", "hashtable", "intset"
```
```go
// Go 中使用 Redigo 库观察 Redis 行为
import "github.com/gomodule/redigo/redis"
conn, _ := redis.Dial("tcp", "localhost:6379")
defer conn.Close()
// HSET 少量数据 → ziplist/hashmap
conn.Do("HSET", "user:1", "name", "alice", "age", "25")
// 数据量大后自动切 hashtable —— 用户无感知
```
## 实践场景
1. **Hash 内存优化**:对于一个用户有几十个字段的小 Hash,手动合并成单个 Key(如 `user:1:name`)不如用一个 Hash 结构 `user:1`,因为 ziplist 能省大量内存。
2. **ZSet 排行榜实现**:电商商品销量排名、社交媒体点赞排行等场景天然适合 ZSet。`ZREVRANGEBYSCORE` 可以高效取 Top-N。
3. **List 替代方案**:如果 List 只用两端操作(LPUSH + RPOP),本质上是个队列;但 quicklist 的中间节点插入在大数据量时会有 O(N) 成本,考虑用 Stream 替代。
## 关联笔记
- [[03.Redis/core/RDB 与 AOF 持久化]]
- [[03.Redis/core/集群与哨兵机制]]
- [[03.Redis/strategies/多级缓存架构设计]]
- [[03.Redis/strategies/缓存穿透击穿雪崩解决方案]]