所有的结构
- Redis 里每个值都是一个 redisObject(简称 robj),它是所有类型的公共外壳:
c
typedef struct redisObject {
unsigned type:4; // 类型:STRING / LIST / HASH / SET / ZSET
unsigned encoding:4; // 编码 ← OBJECT ENCODING 读的就是这个字段
unsigned lru:24; // LRU/LFU 时间戳(内存淘汰用)
int refcount; // 引用计数(共享对象、写时复制用)
void *ptr; // 指针 → 真正的底层数据结构
} robj;
- 五种 type,八种 encoding
| type | encoding(OBJECT ENCODING 打印值) | 真正的 C 数据结构 | 触发条件 |
|---|---|---|---|
| String | int | robj->ptr 直接存 long 值 | 值是 64 位整数 |
| String | embstr | robj + sdshdr8 一次分配 | 字符串 ≤ 44 字节 |
| String | raw | robj 与 sdshdr 分开分配 | 字符串 > 44 字节 |
| List | quicklist | 双向链表,节点内装 listpack | 恒定(3.2+) |
| Hash | listpack(旧版叫 ziplist) | 连续内存紧凑表 | 字段 ≤128 且 值 ≤64 字节 |
| Hash | hashtable | dict(哈希表) | 超过阈值 |
| Set | intset | 有序整数数组 | 全整数 且 元素 ≤512 |
| Set | hashtable | dict(value 全为 NULL) | 超过阈值 |
| ZSet | listpack(旧版 ziplist) | 连续内存紧凑表 | 元素 ≤128 且 member ≤64 字节 |
| ZSet | skiplist | skiplist + dict 联合体 | 超过阈值 |
String
int编码
c
SET count 12345
↓
robj { type=STRING, encoding=INT, ptr=(void*)12345 }
- 就是存数字的,比较好INCR等操作
embstr和raw编码
-
这两底层都是SDS,差别在内存分配次数
embstr(≤44 字节):
┌──────────────────────────────────┐
│ robj(16B) │ sdshdr8(3B) │ 字符串 │ ← 一次 malloc
└──────────────────────────────────┘raw(>44 字节):
┌───────────┐ ┌──────────────────────┐
│ robj(16B) │ ─ptr→ │ sdshdr16/32/64 │ 字符串 │ ← 两次 malloc
└───────────┘ └──────────────────────┘ -
embstr 只读一次缓存行就拿到所有元数据
-
切换点:embstr 是只读优化,一旦修改(APPEND/SETRANGE)就转成 raw,不再转回来
SDS
- SDS(Simple Dynamic String)是 Redis 自己实现的动态字符串结构,用来替代 C 语言原生的 char* 字符串
c
struct sdshdr8 {
uint8_t len; // 已使用字节数(不含 \0)
uint8_t alloc; // 实际分配的字节数(不含 \0 和 header)
unsigned char flags; // 低 3 位标记是 sdshdr8/16/32/64
char buf[]; // 真正存字符串的地方,末尾永远有 \0
};
- 解决了
- 多一个字段len来记录长度
- 用len 字段记录长度,不用遍历到/0就可以知道长度,buf 里可以随便存 \0
- 拼接字符串的时候会检查 alloc 够不够,不够就自动扩容
- 有一套空间预分配策略,不需要频繁realloc
List
只有quicklist编码
┌─────────────────────────────────────────────────────────┐
│ robj(类型壳) │
│ type = REDIS_LIST ← 标识这是一个 List │
│ encoding = OBJ_ENCODING_QUICKLIST ← 当前唯一编码 │
│ ptr ──────────────────┐ │
└─────────────────────────┼───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ quicklist(链表层) │
│ │
│ quicklistNode ──→ quicklistNode ──→ quicklistNode │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ listpack │ │ listpack │ │ listpack │ │
│ │ 8KB max │ │ 8KB max │ │ 8KB max │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ 每个节点里的 listpack 是一段连续内存 │
└─────────────────────────────────────────────────────────┘
-
List 的真实编码就是 quicklist,listpack 只是 quicklist 内部每个节点存数据用的格式
-
listpack 是 Redis 7.0 引入的紧凑连续内存结构,用来替代旧的 ziplist(解决了 ziplist 的连锁更新问题)它把多个元素连续存放在一个数组里:
一个 listpack 内存布局(存 ["a", "bb", "ccc"]):
┌────┬──────┬────┬─────┬─────┬─────┬─────┬─────┬──────┐
│total│entry1│len1│entry2│len2 │entry3│len3 │back │ │
│bytes│ "a" │ 1 │ "bb" │ 2 │"ccc" │ 3 │log │ 0 │
│ 2B │ 1B │1B │ 2B │1B │ 3B │1B │ 2B │ 1B │
└────┴──────┴────┴─────┴─────┴─────┴─────┴─────┴──────┘
↑ 一段连续内存,中间没有缝隙 -
核心优势:元素在内存里紧挨着放,CPU 访问时缓存命中率高
-
如果整个 List 就是一个超大的 listpack,每次中间插入都要搬移大量内存。所以 quicklist 把它切成多个小段:
Hash
- 记住Hash 本质是 field → value 的映射关系
listpack编码
HSET user:1 name zhang age 20
内存布局:
┌─────────────────────────────────────────────────────────────┐
│ entry1 │ entry2 │ entry3 │ entry4 │ entry5 │ entry6 │
│ "name" │ "zhang" │ "age" │ "20" │ (backlog) │
│ 4B │ 5B │ 3B │ 2B │ ... │
└─────────────────────────────────────────────────────────────┘
↑ 一整块连续内存,没有任何指针
- field 和 value 交替存放
- 内存紧凑,无指针开销
- 遍历时缓存命中率极高
hashtable / dict编码
┌─────────────────────────────────────────────────────────────┐
│ dict │
│ │
│ ht[0] ──→ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ 0 │ 1 │ 2 │ 3 │ ... │ │65535│ │
│ └──┬──┴─────┴──┬──┴─────┴─────┴─────┴─────┘ │
│ │ │ │
│ ▼ ▼ │
│ dictEntry dictEntry ──→ dictEntry │
│ {field,val} {field,val} {field,val} │
│ NULL next next │
│ │
│ ht[1] ──→ (rehash 时才使用,平时是空的) │
└─────────────────────────────────────────────────────────────┘
- 当 Hash 字段增多,listpack 的线性查找太慢,必须换成 hashtable
Set
- Set 是无序、元素唯一的集合
intset(纯整数小集合)
SADD myset 1 2 3 5 8
内存布局:
┌──────────────┬─────────┬───────────────────────────┐
│ header │ │ data │
├──────┬───────┼─────────┼─────┬─────┬─────┬─────┬───┤
│encoding│ len │ 保留 │ 1 │ 2 │ 3 │ 5 │ 8 │
│ 2B │ 2B │ 2B │ 2B │ 2B │ 2B │ 2B │2B │
└──────┴───────┴─────────┴─────┴─────┴─────┴─────┴───┘
↑ 整块连续内存,元素按值从小到大排序
- 排序是因为查找的时候是用二分查找,比较快
hashtable(转成 dict)
SADD myset a b c d ... (513个元素)
转换后:
robj → dict → ht[0] → [桶数组] → dictEntry{key, val}
↑
val = NULL(Set 只需要 key)
- Set 用 dict 但 value 是 NULL,因为Set只需要判断存不存在
ZSet
- ZSet(有序集合)
- 要满足查某一个,也要满足查范围
只用 dict(哈希表)编码
dict: member → score
┌────────────────────────────────────┐
│ zhang → 90 │
│ li → 85 │
│ wang → 88 │
│ ... │
└────────────────────────────────────┘
ZSCORE zhang → O(1) ✓ 完美
ZRANGEBYSCORE 80 90:
遍历整个 dict(O(N))
筛选分数在 80-90 的
排序(O(M logM))
总复杂度:O(N + M logM) ✗ 太慢
只用 skiplist(跳表)编码
skiplist(按 score 排序):
Level 2: head ──────────────────────────→ NULL
Level 1: head ──→ [li:85] ────→ [wang:88] ──→ [zhang:90] ──→ NULL
Level 0: head ──→ [li:85] ──→ [wang:88] ──→ [zhang:90] ──→ NULL
ZRANGEBYSCORE 80 90 → O(logN + M) ✓ 完美
ZSCORE zhang:
从头遍历找 zhang(跳表按 score 排序,不是按 member)
平均 O(N) ✗ 太慢
- 跳表的致命弱点:它按 score 排序,无法快速定位到某个 member。
补充一下跳表是什么
-
在有序链表上再叠几层"快线",越上层节点越稀疏、跨得越远
Level 3: 1 ───────────────────────→ 9
Level 2: 1 ────────→ 5 ──────────→ 9
Level 1: 1 ─→ 3 ─→ 5 ─→ 7 ─→ 9 ← 最底层,全部元素
从最高层最左开始:
1.往右走,如果右边的节点仍然 ≤ 目标,就继续走;
2.否则下沉一层继续;
3.到达第 1 层还找不到就是不存在。
Redis两个都用
c
ZADD rank 90 zhang 85 li 88 wang
-
实际存的
┌─────────────────────────────────────────────────────────────┐
│ dict │
│ "zhang" → score=90 │
│ "li" → score=85 │
│ "wang" → score=88 │
└─────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────┐
│ skiplist │
│ │
│ Level 2: head ────────────────────────────→ [zhang:90] │
│ Level 1: head ──→ [li:85] ──────→ [wang:88] → [zhang:90] │
│ Level 0: head ──→ [li:85] → [wang:88] → [zhang:90] → NULL │
│ ↑ │
│ score 从小到大排序 │
└─────────────────────────────────────────────────────────────┘ -
两种查询各走各的路