5.9 KiB
tags, create time, update time
| tags | create time | update time | |||||
|---|---|---|---|---|---|---|---|
|
2026-08-08 18:43 | 2026-08-08 18:43 |
Redis 五大核心数据结构
概述
Redis 的性能基石在于其精心设计的底层数据结构。Redis 的每个值都有多种编码(encoding),会根据数据规模和内容自动切换,在内存占用和操作效率之间寻找最优解。理解这些编码的切换策略,是面试中的高频考点,也是性能调优的关键前提。
核心原理
String — SDS(Simple Dynamic String)
Redis 的 String 类型不是直接用 C 字符串,而是用 SDS(Simple Dynamic String)结构。SDS 的定义如下:
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 伪代码示意 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 编码切换逻辑非常直观:
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 对象的内部编码:
# Redis CLI 中查看键的内部编码
127.0.0.1:6379> OBJECT ENCODED mykey
"ziplist" # 或 "skiplist", "hashtable", "intset"
// 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 —— 用户无感知
实践场景
-
Hash 内存优化:对于一个用户有几十个字段的小 Hash,手动合并成单个 Key(如
user:1:name)不如用一个 Hash 结构user:1,因为 ziplist 能省大量内存。 -
ZSet 排行榜实现:电商商品销量排名、社交媒体点赞排行等场景天然适合 ZSet。
ZREVRANGEBYSCORE可以高效取 Top-N。 -
List 替代方案:如果 List 只用两端操作(LPUSH + RPOP),本质上是个队列;但 quicklist 的中间节点插入在大数据量时会有 O(N) 成本,考虑用 Stream 替代。