| 类型 | 底层数据结构 | 编码切换条件 |
|---|---|---|
| String | int / embstr / raw | 按值大小和类型切换 |
| Hash | listpack / hashtable | 元素少用 listpack,多了用 hashtable |
| List | quicklist(7.0+) | 历史用过 ziplist + linkedlist |
| Set | intset / hashtable | 全整数用 intset,否则 hashtable |
| ZSet | listpack / skiplist+hashtable | 元素少用 listpack,多了用 skiplist |
String
值类型判断:
├── 整数 → int 编码
├── 字符串 ≤ 44 字节 → embstr 编码
└── 字符串 > 44 字节 → raw 编码
| 编码 | 底层结构 | 条件 | 说明 |
|---|---|---|---|
| int | long 整数 | 值为整数且 ≤ long 范围 | 直接存 SDS header + 数字 |
| embstr | SDS + RedisObject 连续内存 | 字符串 ≤ 44 字节 | 一次内存分配,缓存友好 |
| raw | SDS + RedisObject 分离内存 | 字符串 > 44 字节 | 两次内存分配 |
embstr: [RedisObject | SDS header | data] ← 一块连续内存
raw: [RedisObject] → 指针 → [SDS header | data] ← 两块内存
44 字节阈值原因:64 字节 jemalloc 分配,减去 RedisObject(16B) + SDS header(3B) + 结尾 \0(1B) + 预留(4B) ≈ 44B
Hash
| 编码 | 底层结构 | 切换条件 |
|---|---|---|
| listpack | 紧凑连续存储 | 元素数 < 128 且 每个 entry < 64 字节 |
| hashtable | 哈希表 | 超过任一阈值则切换 |
listpack 编码:
┌─────────────────────────────────┐
│ field1 | value1 | field2 | value2 │ ← 连续内存,省空间
└─────────────────────────────────┘
hashtable 编码:
┌──────────┐
│ dictEntry │ → field → value
│ dictEntry │ → field → value
│ ... │
└──────────┘
| 特性 | listpack | hashtable |
|---|---|---|
| 内存占用 | 极小 | 有额外指针开销 |
| 查找复杂度 | O(n) | O(1) |
| 适用场景 | 少量元素 | 大量元素 |
List
| 版本 | 底层结构 | 说明 |
|---|---|---|
| Redis 7.0+ | quicklist | 双向链表,每个节点是一个 listpack |
| Redis 3.2~6.x | quicklist | 双向链表 + ziplist 节点 |
| Redis < 3.2 | ziplist / linkedlist | 按条件切换 |
quicklist 结构:
head ←→ [listpack node] ←→ [listpack node] ←→ [listpack node] ←→ tail
┌──────────┐ ┌──────────┐
│ item1 │ │ item4 │
│ item2 │ │ item5 │
│ item3 │ │ item6 │
└──────────┘ └──────────┘
(连续内存块) (连续内存块)
| 特性 | 说明 |
|---|---|
| 兼顾内存和性能 | 小数据用 listpack 压缩省内存,大数据用链表保证操作效率 |
| 单节点 listpack 大小 | 默认 8KB,可配置 list-max-listpack-size |
| 支持 LPOP/RPOP | O(1) |
| 支持 LRANGE | O(n) |
Set
| 编码 | 底层结构 | 切换条件 |
|---|---|---|
| intset | 有序整数数组 | 所有元素都是整数 且 元素数 ≤ 512 |
| hashtable | 哈希表 | 非整数元素 或 元素数 > 512 |
intset 编码:
┌───────────────────────────────────────────┐
│ header | 1 | 5 | 10 | 20 │ ← 有序整数数组,支持二分查找
└───────────────────────────────────────────┘
升级: int16 → int32 → int64(按最大元素决定编码)
hashtable 编码:
┌──────────┐
│ dictEntry │ → "apple" (value=NULL)
│ dictEntry │ → "banana"
│ ... │
└──────────┘
| 特性 | intset | hashtable |
|---|---|---|
| 内存 | 极省(无指针开销) | 有指针开销 |
| 查找 | O(log n) 二分查找 | O(1) |
| 元素类型 | 仅整数 | 任意 |
| 编码升级 | int16→int32→int64 | --- |
ZSet(Sorted Set)
| 编码 | 底层结构 | 切换条件 |
|---|---|---|
| listpack | 紧凑连续存储 | 元素数 < 128 且 每个 entry < 64 字节 |
| skiplist + hashtable | 跳表 + 哈希表 | 超过任一阈值 |
skiplist 编码(双结构组合):
┌─────────────────────────────────────────────────┐
│ SkipList(跳表) │
│ │
│ L3: head ──────────────────→ 30 │
│ L2: head ─────→ 10 ───────→ 30 │
│ L1: head → 5 → 10 → 20 ──→ 30 │
│ L0: head → 5 → 10 → 20 → 30 → 40 │
│ │
│ 每个 node: [member, score, forward[], backward] │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Dict(哈希表) │
│ │
│ "apple" → score=10 │
│ "banana" → score=20 │
│ "cherry" → score=5 │
│ │
│ 作用: O(1) 查 score │
└─────────────────────────────────────────────────┘
| 操作 | 用哪个结构 | 复杂度 |
|---|---|---|
| ZADD | 跳表插入 + 哈希表更新 | O(log n) |
| ZSCORE | 哈希表 | O(1) |
| ZRANK | 跳表回溯 | O(log n) |
| ZRANGE | 跳表遍历 | O(log n + m) |
为什么用两个结构? 跳表支持范围查询,哈希表支持 O(1) 查分数。两者配合覆盖所有操作。
为什么用跳表而不是红黑树?
| 对比 | 跳表 (SkipList) | 红黑树 (RBTree) |
|---|---|---|
| 范围查询 | ✅ 天然支持,链表顺序遍历 | ❌ 需要中序遍历,复杂 |
| 实现复杂度 | 简单(概率性平衡) | 复杂(旋转+变色) |
| 并发友好 | ✅ 局部加锁 | ❌ 旋转影响范围大 |
| 内存 | 额外多级指针 | 每节点额外颜色位 |
| 缓存友好 | 较好 | 一般 |
Redis 作者 Antirez 的选择理由:实现简单 + 范围查询天然支持 + 并发友好。
总结速查
String: int / embstr / raw
Hash: listpack / hashtable
List: quicklist
Set: intset / hashtable
ZSet: listpack / skiplist + hashtable
| 设计原则 | 体现 |
|---|---|
| 小数据省内存 | listpack / intset 紧凑存储 |
| 大数据保性能 | hashtable / skiplist 保证 O(1) 或 O(log n) |
| 编码自动切换 | 根据元素数量和大小自动升级,对用户透明 |
| 空间换时间 | ZSet 同时维护跳表+哈希表 |