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

149 lines
5.9 KiB
Markdown
Raw Normal View History

2026-08-08 19:01:04 +08:00
---
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/缓存穿透击穿雪崩解决方案]]