Redis 从入门到精通(二):深入数据结构 ------ 从 RedisObject 到 SkipList 的源码级拆解
一、万物之源:RedisObject
在深入各种数据结构之前,必须先理解一个核心概念------Redis 中所有 key 和 value 都被封装为 redisObject。
c
// Redis 7.0 源码 src/server.h
typedef struct redisObject {
unsigned type:4; // 数据类型(string, list, hash, set, zset)
unsigned encoding:4; // 内部编码(int, embstr, raw, ziplist, skiplist 等)
unsigned lru:24; // LRU 时钟 或 LFU 计数器
int refcount; // 引用计数
void *ptr; // 指向实际数据的指针
} robj;
这个结构只有 16 字节(在 64 位系统上),但它是理解 Redis 内存管理的钥匙。
| 字段 | 位宽 | 作用 |
|---|---|---|
type |
4 bit | 对外暴露的逻辑类型:OBJ_STRING(0), OBJ_LIST(1), OBJ_HASH(4), OBJ_SET(2), OBJ_ZSET(3) |
encoding |
4 bit | 底层实际编码:OBJ_ENCODING_INT(1), OBJ_ENCODING_EMBSTR(8), OBJ_ENCODING_RAW(0), OBJ_ENCODING_ZIPLIST(5), OBJ_ENCODING_SKIPLIST(7) 等 |
lru |
24 bit | 内存淘汰用,存储 LRU 时钟(秒级)或 LFU 计数(高 16 位存分钟级时间戳 + 低 8 位存对数计数器) |
refcount |
32 bit | 引用计数,用于对象共享和内存回收 |
ptr |
64 bit | 指向底层数据结构的指针 |
关键设计思想 :type 和 encoding 是解耦的。一个 Hash 类型的 key,底层可能是 ziplist 也可能是 hashtable。Redis 会根据数据量和元素大小自动切换编码,在内存占用和操作性能之间找到最优平衡。
#mermaid-svg-KQZ6d4kfeVNc8SVJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .error-icon{fill:#552222;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .marker.cross{stroke:#333333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ p{margin:0;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster-label text{fill:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster-label span{color:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster-label span p{background-color:transparent;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .label text,#mermaid-svg-KQZ6d4kfeVNc8SVJ span{fill:#333;color:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .node rect,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node circle,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node ellipse,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node polygon,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .rough-node .label text,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node .label text,#mermaid-svg-KQZ6d4kfeVNc8SVJ .image-shape .label,#mermaid-svg-KQZ6d4kfeVNc8SVJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .rough-node .label,#mermaid-svg-KQZ6d4kfeVNc8SVJ .node .label,#mermaid-svg-KQZ6d4kfeVNc8SVJ .image-shape .label,#mermaid-svg-KQZ6d4kfeVNc8SVJ .icon-shape .label{text-align:center;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .node.clickable{cursor:pointer;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .arrowheadPath{fill:#333333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KQZ6d4kfeVNc8SVJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KQZ6d4kfeVNc8SVJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster text{fill:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .cluster span{color:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KQZ6d4kfeVNc8SVJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .icon-shape,#mermaid-svg-KQZ6d4kfeVNc8SVJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .icon-shape p,#mermaid-svg-KQZ6d4kfeVNc8SVJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .icon-shape .label rect,#mermaid-svg-KQZ6d4kfeVNc8SVJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KQZ6d4kfeVNc8SVJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KQZ6d4kfeVNc8SVJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KQZ6d4kfeVNc8SVJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内部编码(自动切换)
对外暴露
String
Hash
List
Set
ZSet
int / embstr / raw
listpack / hashtable
quicklist
intset / listpack / hashtable
listpack / skiplist+dict
一句话 :
type决定了用户用什么命令操作它,encoding决定了数据在内存中怎么存储。
二、SDS:Redis 的字符串引擎
Redis 没有使用 C 语言原生的 char* 字符串(以 \0 结尾),而是自己实现了一套 SDS(Simple Dynamic String)。这背后藏着 Redis 对性能和安全性的极致追求。
2.1 C 字符串的四大痛点
c
// C 原生字符串的问题
char *str = "hello";
strlen(str); // O(n) ------ 每次获取长度都要遍历
strcat(str, " world"); // 缓冲区溢出风险
// 存二进制数据时,遇到 \0 就截断
| 痛点 | C 字符串 | SDS 解决方案 |
|---|---|---|
| 获取长度 | O(n) 遍历 | O(1),用 len 字段直接返回 |
| 缓冲区溢出 | 不记录容量,strcat 可能溢出 |
自动扩容,alloc 字段记录已分配空间 |
| 二进制不安全 | \0 结尾,存图片/序列化数据会截断 |
用 len 判断结束,而非 \0,可存任意二进制 |
| 频繁扩容 | 每次修改可能触发 realloc |
预分配策略 + 惰性释放,减少内存重分配次数 |
2.2 SDS 的结构演进
Redis 3.2 之前,SDS 只有一个简单结构:
c
// 旧版 SDS(Redis < 3.2)
struct sdshdr {
int len; // 已使用长度
int free; // 剩余可用空间
char buf[]; // 柔性数组,存放实际数据
};
这个设计的浪费在于:即使只存 3 个字节的字符串 "abc",len 和 free 两个 int 就要 8 字节。Redis 3.2 以后根据字符串长度使用了 5 种不同类型的 SDS 头:
c
// Redis 7.0 src/sds.h ------ 按长度分级
struct __attribute__ ((__packed__)) sdshdr5 { // 未使用,仅用于标记
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // len < 2^8 (256B)
uint8_t len; // 1 字节
uint8_t alloc; // 1 字节
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 { // len < 2^16 (64KB)
uint16_t len; // 2 字节
uint16_t alloc; // 2 字节
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // len < 2^32 (4GB)
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 { // len < 2^64
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
__attribute__ ((__packed__))告诉编译器取消内存对齐,让结构体紧凑排列,节省内存。这对于存海量小字符串的场景非常关键。
2.3 空间预分配与惰性释放
SDS 的扩容不是 len = new_len 那么简单,而是有一套经验策略:
c
// 扩容算法(src/sds.c)
newlen += len; // 至少加上原长度
if (newlen < 1024*1024) // 小于 1MB
newlen *= 2; // 翻倍扩容
else
newlen += 1024*1024; // 超过 1MB 后,每次只加 1MB
这保证了:
- 小字符串 :翻倍扩容,减少频繁
realloc,典型空间换时间 - 大字符串:线性增长,避免内存浪费过大
释放时也不立即归还内存,而是修改 len 并保留 alloc 空间,留给下次写入复用。
三、String 的三种内部编码:int / embstr / raw
有了 SDS 的铺垫,String 的三种编码就很好理解了。
#mermaid-svg-kD6bYQLJVTRNPbuz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kD6bYQLJVTRNPbuz .error-icon{fill:#552222;}#mermaid-svg-kD6bYQLJVTRNPbuz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kD6bYQLJVTRNPbuz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kD6bYQLJVTRNPbuz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kD6bYQLJVTRNPbuz .marker.cross{stroke:#333333;}#mermaid-svg-kD6bYQLJVTRNPbuz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kD6bYQLJVTRNPbuz p{margin:0;}#mermaid-svg-kD6bYQLJVTRNPbuz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster-label text{fill:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster-label span{color:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster-label span p{background-color:transparent;}#mermaid-svg-kD6bYQLJVTRNPbuz .label text,#mermaid-svg-kD6bYQLJVTRNPbuz span{fill:#333;color:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz .node rect,#mermaid-svg-kD6bYQLJVTRNPbuz .node circle,#mermaid-svg-kD6bYQLJVTRNPbuz .node ellipse,#mermaid-svg-kD6bYQLJVTRNPbuz .node polygon,#mermaid-svg-kD6bYQLJVTRNPbuz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kD6bYQLJVTRNPbuz .rough-node .label text,#mermaid-svg-kD6bYQLJVTRNPbuz .node .label text,#mermaid-svg-kD6bYQLJVTRNPbuz .image-shape .label,#mermaid-svg-kD6bYQLJVTRNPbuz .icon-shape .label{text-anchor:middle;}#mermaid-svg-kD6bYQLJVTRNPbuz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kD6bYQLJVTRNPbuz .rough-node .label,#mermaid-svg-kD6bYQLJVTRNPbuz .node .label,#mermaid-svg-kD6bYQLJVTRNPbuz .image-shape .label,#mermaid-svg-kD6bYQLJVTRNPbuz .icon-shape .label{text-align:center;}#mermaid-svg-kD6bYQLJVTRNPbuz .node.clickable{cursor:pointer;}#mermaid-svg-kD6bYQLJVTRNPbuz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kD6bYQLJVTRNPbuz .arrowheadPath{fill:#333333;}#mermaid-svg-kD6bYQLJVTRNPbuz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kD6bYQLJVTRNPbuz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kD6bYQLJVTRNPbuz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kD6bYQLJVTRNPbuz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kD6bYQLJVTRNPbuz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kD6bYQLJVTRNPbuz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster text{fill:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz .cluster span{color:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kD6bYQLJVTRNPbuz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kD6bYQLJVTRNPbuz rect.text{fill:none;stroke-width:0;}#mermaid-svg-kD6bYQLJVTRNPbuz .icon-shape,#mermaid-svg-kD6bYQLJVTRNPbuz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kD6bYQLJVTRNPbuz .icon-shape p,#mermaid-svg-kD6bYQLJVTRNPbuz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kD6bYQLJVTRNPbuz .icon-shape .label rect,#mermaid-svg-kD6bYQLJVTRNPbuz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kD6bYQLJVTRNPbuz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kD6bYQLJVTRNPbuz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kD6bYQLJVTRNPbuz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} raw 编码
ptr 指针指向
redisObject
sdshdr + buf
两次 malloc
内存不连续
embstr 编码
redisObject + sdshdr8 + buf
一次 malloc
连续内存 64 字节内
int 编码
redisObject
ptr 直接存整数
8 字节
零额外分配
int 编码
当 value 可以被解析为 long 范围内的整数时,ptr 指针直接存储整数值,而不是指向某个数据结构。这是最省内存的方式------仅 16 字节的 redisObject。
bash
SET num 10086
OBJECT ENCODING num # 返回 "int"
embstr 编码
字符串长度 ≤ 44 字节时使用。RedisObject 和 SDS 在一次 malloc 中连续分配,内存紧凑、缓存友好。注意:embstr 是只读的,任何修改都会退化为 raw。
bash
SET name "hello"
OBJECT ENCODING name # 返回 "embstr"
APPEND name " world" # 修改后触发退化
OBJECT ENCODING name # 返回 "raw"
raw 编码
字符串长度 > 44 字节时使用。RedisObject 和 SDS 分开分配,通过指针关联。
为什么是 44 字节?
这个数字来源于 Redis 使用的 jemalloc 内存分配器:
jemalloc 分配单元:64 字节(小对象的最小分配单元)
- redisObject: 16 字节
- sdshdr8: 3 字节(len 1B + alloc 1B + flags 1B)
- SDS 结尾 \0: 1 字节
- 剩余给 buf: 64 - 16 - 3 - 1 = 44 字节
刚好卡在一个 jemalloc 分配单元内,不多不少,0 浪费。这就是 Redis 开发者对内存的精打细算。
四、Ziplist:Redis 紧凑存储的经典之作
ziplist 是 Redis 中应用最广泛的紧凑数据结构,被 List、Hash、Sorted Set 共享使用。它的设计目标是:极致的空间利用率,代价是 O(n) 的操作复杂度。
4.1 内存布局
ziplist 整体结构(小端字节序):
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
4B 4B 2B ↑ 1B (0xFF)
每个 entry 变长编码
| 字段 | 大小 | 说明 |
|---|---|---|
zlbytes |
4 字节 | 整个 ziplist 占用的字节数,方便直接 realloc |
zltail |
4 字节 | 最后一个 entry 的偏移量,支持 O(1) 尾部操作 |
zllen |
2 字节 | entry 数量,超过 65535 时需要遍历获取 |
entry |
变长 | 实际存储的数据节点 |
zlend |
1 字节 | 固定 0xFF,标记结尾 |
每个 entry 的结构:
<prevlen> <encoding> <entry-data>
变长 变长 变长
prevlen: 前一个 entry 的长度
- 如果前一个 entry < 254 字节 → prevlen 占 1 字节
- 如果前一个 entry ≥ 254 字节 → prevlen 占 5 字节(首字节 0xFE + 4 字节长度)
encoding: 编码类型和数据长度
- 00xxxxxx → 6 位存小整数
- 01xxxxxx <4字节len> → 字符串,最多 2^14-1
- 10xxxxxx <4字节len> → 字符串,最多 2^32-1
- 11000000 → int16
- 11010000 → int32
- 11100000 → int64
- 11110000 → 24 位整数
- 11111110 → 8 位整数
- 1111xxxx → 4 位整数(xxxx 直接存值,无需额外字节)
4.2 连锁更新 ------ ziplist 的阿喀琉斯之踵
这是 Redis 面试的经典题目。假设一个 ziplist 中有 N 个 entry,每个长度都是 253 字节(prevlen 占 1 字节)。现在在头部插入一个 254 字节的新 entry,会发生什么?
插入前(所有 prevlen 都是 1 字节):
[254B新entry] [253B] [253B] [253B] ... [253B]
插入后第一个 253B entry 的 prevlen 需要 5 字节(因为前一个 ≥ 254B)
→ 第一个 253B entry 膨胀为 257B(253+4)
→ 第二个 253B entry 的 prevlen 也变为 5 字节
→ 第二个 253B entry 膨胀为 257B
→ 第三个...
→ 连锁反应,所有后续 entry 都要更新!
这就是 连锁更新(Cascade Update)------最坏情况下时间复杂度 O(N²),这也是 Redis 要引入 listpack 替代 ziplist 的根本原因。
五、Listpack:Redis 7.0 的答案
listpack 是 Redis 专门设计用来替代 ziplist 的新结构。核心改进只有一点:去掉 prevlen 字段,彻底消除连锁更新。
5.1 内存布局
listpack 结构:
<total_bytes> <num_elements> <element> <element> ... <element> <end>
4B 2B ↑ 1B (0xFF)
每个 element 包含自身长度
每个 element 结构:
<encoding-type> <data> <element-tot-len>
变长 变长 变长(存当前 element 总长度,替代 prevlen)
5.2 与 ziplist 的关键差异
| 特性 | ziplist | listpack |
|---|---|---|
| 定位前一个节点 | 靠 prevlen(存前一个节点长度) |
不存前一个节点长度,无法向前遍历 |
| 向前遍历 | 支持,O(1) | 间接支持------通过 element-tot-len 向后跳转回退 |
| 连锁更新 | 存在,最坏 O(N²) | 完全不存在 |
| 每个 entry 额外开销 | prevlen(1 或 5 字节) |
element-tot-len(变长,通常 1 字节) |
| 支持的操作 | 支持从两端操作(List 需要) | 主要用于 Hash 和 ZSet 的小数据场景 |
关键 :listpack 只存当前节点的长度。向前遍历时,从后往前,读到
element-tot-len,直接用这个长度向前跳。虽然比 ziplist 的 prevlen 多了一步"读取上一个节点尾部",但换来了彻底消灭连锁更新的安全性。
5.3 过渡方案:listpack 替代 ziplist 的配置
bash
# Redis 7.0 默认配置 ------ 已全部使用 listpack
hash-max-listpack-entries 512
hash-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64
# Redis 7.2 彻底移除了 ziplist 相关的配置项
六、Quicklist:List 的王炸组合
第一篇我们提到了 quicklist 是双向链表 + ziplist/listpack 的混合结构,现在深入看看它的设计细节。
6.1 结构定义
c
// 快速列表节点(src/quicklist.h)
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *entry; // 指向 ziplist 或 listpack
size_t sz; // entry 的字节数
unsigned int count : 16; // entry 中的元素个数
unsigned int encoding : 2; // RAW=1(ziplist)或 LZF=2(压缩)
unsigned int container : 2;// PLAIN=1(ziplist)或 PACKED=2(listpack)
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int dont_compress : 1;
unsigned int extra : 9;
} quicklistNode;
// 快速列表主结构
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; // 所有 entry 中的元素总数
unsigned long len; // quicklistNode 的数量
signed int fill : QL_FILL_BITS; // 每个节点的填充因子
unsigned int compress : QL_COMP_BITS; // 两端不压缩的深度
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
#mermaid-svg-1uQXWUdFq2ZXM6BN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1uQXWUdFq2ZXM6BN .error-icon{fill:#552222;}#mermaid-svg-1uQXWUdFq2ZXM6BN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1uQXWUdFq2ZXM6BN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .marker.cross{stroke:#333333;}#mermaid-svg-1uQXWUdFq2ZXM6BN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1uQXWUdFq2ZXM6BN p{margin:0;}#mermaid-svg-1uQXWUdFq2ZXM6BN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster-label text{fill:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster-label span{color:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster-label span p{background-color:transparent;}#mermaid-svg-1uQXWUdFq2ZXM6BN .label text,#mermaid-svg-1uQXWUdFq2ZXM6BN span{fill:#333;color:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .node rect,#mermaid-svg-1uQXWUdFq2ZXM6BN .node circle,#mermaid-svg-1uQXWUdFq2ZXM6BN .node ellipse,#mermaid-svg-1uQXWUdFq2ZXM6BN .node polygon,#mermaid-svg-1uQXWUdFq2ZXM6BN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .rough-node .label text,#mermaid-svg-1uQXWUdFq2ZXM6BN .node .label text,#mermaid-svg-1uQXWUdFq2ZXM6BN .image-shape .label,#mermaid-svg-1uQXWUdFq2ZXM6BN .icon-shape .label{text-anchor:middle;}#mermaid-svg-1uQXWUdFq2ZXM6BN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .rough-node .label,#mermaid-svg-1uQXWUdFq2ZXM6BN .node .label,#mermaid-svg-1uQXWUdFq2ZXM6BN .image-shape .label,#mermaid-svg-1uQXWUdFq2ZXM6BN .icon-shape .label{text-align:center;}#mermaid-svg-1uQXWUdFq2ZXM6BN .node.clickable{cursor:pointer;}#mermaid-svg-1uQXWUdFq2ZXM6BN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .arrowheadPath{fill:#333333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1uQXWUdFq2ZXM6BN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1uQXWUdFq2ZXM6BN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1uQXWUdFq2ZXM6BN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster text{fill:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN .cluster span{color:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1uQXWUdFq2ZXM6BN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1uQXWUdFq2ZXM6BN rect.text{fill:none;stroke-width:0;}#mermaid-svg-1uQXWUdFq2ZXM6BN .icon-shape,#mermaid-svg-1uQXWUdFq2ZXM6BN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1uQXWUdFq2ZXM6BN .icon-shape p,#mermaid-svg-1uQXWUdFq2ZXM6BN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1uQXWUdFq2ZXM6BN .icon-shape .label rect,#mermaid-svg-1uQXWUdFq2ZXM6BN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1uQXWUdFq2ZXM6BN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1uQXWUdFq2ZXM6BN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1uQXWUdFq2ZXM6BN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} quicklist 结构
N1 内部 listpack
element: msg1
element: msg2
element: msg3
head
quicklistNode
count=3
sz=64
quicklistNode
count=3
sz=64
quicklistNode
count=3
sz=64
tail
6.2 fill 参数:控制每个节点的大小
list-max-listpack-size(即 fill 参数)可以是正数或负数:
| 值 | 含义 |
|---|---|
| -5 | 每个节点最大 64 KB |
| -4 | 每个节点最大 32 KB |
| -3 | 每个节点最大 16 KB |
| -2 | 每个节点最大 8 KB(默认值) |
| -1 | 每个节点最大 4 KB |
| 正数 N | 每个节点最多存 N 个元素 |
默认 -2(每节点 8KB),这个值经过 Redis 团队大量测试,在性能和内存之间取得了良好平衡。
6.3 LZF 压缩:懒压缩 + 两端不压缩
quicklist 支持对中间的节点进行 LZF 无损压缩 ,但有个巧妙的规则:两端不压缩。
bash
# list-compress-depth 配置
list-compress-depth 0 # 不压缩(默认)
list-compress-depth 1 # 两端各 1 个节点不压缩
list-compress-depth 2 # 两端各 2 个节点不压缩
为什么两端不压缩?因为 List 的大多数操作都在两端(LPUSH、RPUSH、LPOP、RPOP),保持两端节点未压缩可以避免频繁解压/压缩的性能开销。中间的"冷数据"才压缩,省内存但不影响常用操作。
七、Dict 与渐进式 Rehash
Redis 的字典(dict)是 Hash 类型和整个键空间的基础。它的实现是一个标准的拉链法哈希表,但有两个独门绝技。
7.1 Dict 结构
c
// src/dict.h
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 链表解决冲突
} dictEntry;
typedef struct dictht {
dictEntry **table; // 哈希桶数组
unsigned long size; // 桶数量,总是 2^n
unsigned long sizemask; // size - 1,用于取模
unsigned long used; // 已使用 entry 数量
} dictht;
typedef struct dict {
dictType *type; // 类型特定函数(hash、compare、destructor 等)
void *privdata;
dictht ht[2]; // 两个哈希表,rehash 时使用
long rehashidx; // rehash 进度,-1 表示未在 rehash
int16_t pauserehash; // 暂停 rehash 的标记
} dict;
关键设计:ht2 双表 + rehashidx 渐进式索引。
7.2 哈希算法与冲突解决
Redis 使用 SipHash 1-2 哈希函数(Redis 5.0 后替代了 MurmurHash2),既能保证良好的分布性,又能防止哈希洪水攻击(HashDoS)。
冲突解决方式为经典的拉链法(链地址法),新冲突节点插到链表头部(头插法),O(1) 插入。
c
// 哈希定位
idx = hash_function(key) & ht.sizemask;
// sizemask = size - 1,size 是 2^n,等价于 hash % size,但位运算更快
7.3 渐进式 Rehash ------ 单线程架构下的精妙设计
这是 Redis 最值得称道的设计之一。当负载因子(used / size)触发阈值时,需要扩容,但 Redis 是单线程的,如果一次性把所有 key 迁移到新哈希表,会长时间阻塞服务。
渐进式 Rehash 的做法:
#mermaid-svg-WQT9nqz6kpIqRu6n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WQT9nqz6kpIqRu6n .error-icon{fill:#552222;}#mermaid-svg-WQT9nqz6kpIqRu6n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WQT9nqz6kpIqRu6n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WQT9nqz6kpIqRu6n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WQT9nqz6kpIqRu6n .marker.cross{stroke:#333333;}#mermaid-svg-WQT9nqz6kpIqRu6n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WQT9nqz6kpIqRu6n p{margin:0;}#mermaid-svg-WQT9nqz6kpIqRu6n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster-label text{fill:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster-label span{color:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster-label span p{background-color:transparent;}#mermaid-svg-WQT9nqz6kpIqRu6n .label text,#mermaid-svg-WQT9nqz6kpIqRu6n span{fill:#333;color:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n .node rect,#mermaid-svg-WQT9nqz6kpIqRu6n .node circle,#mermaid-svg-WQT9nqz6kpIqRu6n .node ellipse,#mermaid-svg-WQT9nqz6kpIqRu6n .node polygon,#mermaid-svg-WQT9nqz6kpIqRu6n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WQT9nqz6kpIqRu6n .rough-node .label text,#mermaid-svg-WQT9nqz6kpIqRu6n .node .label text,#mermaid-svg-WQT9nqz6kpIqRu6n .image-shape .label,#mermaid-svg-WQT9nqz6kpIqRu6n .icon-shape .label{text-anchor:middle;}#mermaid-svg-WQT9nqz6kpIqRu6n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WQT9nqz6kpIqRu6n .rough-node .label,#mermaid-svg-WQT9nqz6kpIqRu6n .node .label,#mermaid-svg-WQT9nqz6kpIqRu6n .image-shape .label,#mermaid-svg-WQT9nqz6kpIqRu6n .icon-shape .label{text-align:center;}#mermaid-svg-WQT9nqz6kpIqRu6n .node.clickable{cursor:pointer;}#mermaid-svg-WQT9nqz6kpIqRu6n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WQT9nqz6kpIqRu6n .arrowheadPath{fill:#333333;}#mermaid-svg-WQT9nqz6kpIqRu6n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WQT9nqz6kpIqRu6n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WQT9nqz6kpIqRu6n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WQT9nqz6kpIqRu6n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WQT9nqz6kpIqRu6n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WQT9nqz6kpIqRu6n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster text{fill:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n .cluster span{color:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WQT9nqz6kpIqRu6n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WQT9nqz6kpIqRu6n rect.text{fill:none;stroke-width:0;}#mermaid-svg-WQT9nqz6kpIqRu6n .icon-shape,#mermaid-svg-WQT9nqz6kpIqRu6n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WQT9nqz6kpIqRu6n .icon-shape p,#mermaid-svg-WQT9nqz6kpIqRu6n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WQT9nqz6kpIqRu6n .icon-shape .label rect,#mermaid-svg-WQT9nqz6kpIqRu6n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WQT9nqz6kpIqRu6n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WQT9nqz6kpIqRu6n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WQT9nqz6kpIqRu6n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 步骤 3:rehash 完成
ht0 → 释放
ht1 → 变为 ht0
步骤 2:rehashidx=0
每次增删改查操作
顺带迁移 1 个桶
步骤 1:触发 rehash
ht0 旧表
size=4, used=6
ht1 新表
size=8(空)
核心流程:
- 为
ht[1]分配空间(扩容为ht[0].used * 2的下一个 2^n) - 设置
rehashidx = 0,标记开始 rehash - 每次对 dict 的增删改查操作,都会附带迁移
ht[0]中rehashidx位置的桶到ht[1],然后rehashidx++ - 同时后台有一个定时任务(
serverCron),每次迁移 100 个桶(1ms 内),加速 rehash rehashidx == -1时完成,释放ht[0],ht[1]变为新的ht[0]
查找时需要同时查两张表 :先查 ht[0],没有再查 ht[1]。新增只写 ht[1]。
7.4 触发条件
c
// 扩容条件
if (ht[0].used >= ht[0].size && dict_can_resize) {
// 负载因子 ≥ 1,且没有在进行 BGSAVE/BGREWRITEAOF
dictExpand(d, ht[0].used * 2);
}
if (ht[0].used > ht[0].size * 5) {
// 负载因子 > 5,强制扩容(即使有 BGSAVE 也扩,因为太挤了)
dictExpand(d, ht[0].used * 2);
}
// 缩容条件(used < size * 0.1,非常空闲时才缩)
if (ht[0].used < ht[0].size / 10) {
dictResize(d);
}
这里有个细节:有子进程(BGSAVE/BGREWRITEAOF)时不扩容,除非负载因子 > 5。因为扩容涉及大量内存 copy-on-write,会让父进程内存翻倍,可能导致 OOM。
八、SkipList:跳表,Sorted Set 的核心引擎
第一篇提到了 Sorted Set 使用 dict + skiplist 双索引,现在详细拆解跳表的实现。
8.1 什么是跳表?
跳表(Skip List)是 William Pugh 在 1989 年提出的一种概率性数据结构,通过在有序链表上建立多层索引,将查找复杂度从 O(n) 降到 O(log N)。
#mermaid-svg-RadhCBLNecNgeLxx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RadhCBLNecNgeLxx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RadhCBLNecNgeLxx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RadhCBLNecNgeLxx .error-icon{fill:#552222;}#mermaid-svg-RadhCBLNecNgeLxx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RadhCBLNecNgeLxx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RadhCBLNecNgeLxx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RadhCBLNecNgeLxx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RadhCBLNecNgeLxx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RadhCBLNecNgeLxx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RadhCBLNecNgeLxx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RadhCBLNecNgeLxx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RadhCBLNecNgeLxx .marker.cross{stroke:#333333;}#mermaid-svg-RadhCBLNecNgeLxx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RadhCBLNecNgeLxx p{margin:0;}#mermaid-svg-RadhCBLNecNgeLxx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RadhCBLNecNgeLxx .cluster-label text{fill:#333;}#mermaid-svg-RadhCBLNecNgeLxx .cluster-label span{color:#333;}#mermaid-svg-RadhCBLNecNgeLxx .cluster-label span p{background-color:transparent;}#mermaid-svg-RadhCBLNecNgeLxx .label text,#mermaid-svg-RadhCBLNecNgeLxx span{fill:#333;color:#333;}#mermaid-svg-RadhCBLNecNgeLxx .node rect,#mermaid-svg-RadhCBLNecNgeLxx .node circle,#mermaid-svg-RadhCBLNecNgeLxx .node ellipse,#mermaid-svg-RadhCBLNecNgeLxx .node polygon,#mermaid-svg-RadhCBLNecNgeLxx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RadhCBLNecNgeLxx .rough-node .label text,#mermaid-svg-RadhCBLNecNgeLxx .node .label text,#mermaid-svg-RadhCBLNecNgeLxx .image-shape .label,#mermaid-svg-RadhCBLNecNgeLxx .icon-shape .label{text-anchor:middle;}#mermaid-svg-RadhCBLNecNgeLxx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RadhCBLNecNgeLxx .rough-node .label,#mermaid-svg-RadhCBLNecNgeLxx .node .label,#mermaid-svg-RadhCBLNecNgeLxx .image-shape .label,#mermaid-svg-RadhCBLNecNgeLxx .icon-shape .label{text-align:center;}#mermaid-svg-RadhCBLNecNgeLxx .node.clickable{cursor:pointer;}#mermaid-svg-RadhCBLNecNgeLxx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RadhCBLNecNgeLxx .arrowheadPath{fill:#333333;}#mermaid-svg-RadhCBLNecNgeLxx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RadhCBLNecNgeLxx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RadhCBLNecNgeLxx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RadhCBLNecNgeLxx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RadhCBLNecNgeLxx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RadhCBLNecNgeLxx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RadhCBLNecNgeLxx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RadhCBLNecNgeLxx .cluster text{fill:#333;}#mermaid-svg-RadhCBLNecNgeLxx .cluster span{color:#333;}#mermaid-svg-RadhCBLNecNgeLxx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RadhCBLNecNgeLxx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RadhCBLNecNgeLxx rect.text{fill:none;stroke-width:0;}#mermaid-svg-RadhCBLNecNgeLxx .icon-shape,#mermaid-svg-RadhCBLNecNgeLxx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RadhCBLNecNgeLxx .icon-shape p,#mermaid-svg-RadhCBLNecNgeLxx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RadhCBLNecNgeLxx .icon-shape .label rect,#mermaid-svg-RadhCBLNecNgeLxx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RadhCBLNecNgeLxx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RadhCBLNecNgeLxx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RadhCBLNecNgeLxx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Level 1
Level 2
Level 3
head
45
NIL
23
45
12
23
37
45
67
- 最底层(Level 1)是一个完整的有序双向链表
- 每往上一层都是下一层的"快车道",跳过部分节点
- 每个节点随机决定自己"能到多高"(概率 50%,每层递减)
8.2 Redis 跳表的实现
c
// src/server.h
typedef struct zskiplistNode {
sds ele; // member(字符串)
double score; // 分数
struct zskiplistNode *backward; // 后退指针(从后往前遍历)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度(到下一个节点的距离,用于计算排名)
} level[]; // 柔性数组,长度即节点高度
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾节点
unsigned long length; // 节点总数
int level; // 最大层数
} zskiplist;
核心亮点------span 字段:
span 记录的是当前节点到下一个节点跨越了多少个原始节点。这让 O(log N) 复杂度内完成排名查询成为可能:
假设查 "37" 的排名(降序):
从 header L3 → 45,发现 45 > 37,不能跳(降序找更小的)
降到 L2 → 23,发现 23 < 37,可以跳,排名累计 +span
... 以此类推,到达 37 时,累计的 span 就是排名
8.3 跳表 vs 红黑树
| 维度 | 跳表 | 红黑树 |
|---|---|---|
| 实现复杂度 | 简单(~200 行) | 复杂(旋转、染色,~500 行) |
| 范围查询 | O(log N + M),顺序遍历后继即可 | 需中序遍历或维护链表 |
| 排名查询 | 原生支持(span 字段) | 需额外维护 size 字段 |
| 并发性 | 好,局部修改,可细粒度加锁 | 差,旋转可能影响大量节点 |
| 内存占用 | 略高(多层指针) | 较低(每节点 3 个指针:left/right/parent) |
Redis 选择跳表的逻辑很清晰:Sorted Set 的核心场景就是范围查询和排名,这恰好是跳表的强势领域。而且实现简单意味着 bug 少、维护成本低。
九、Intset:整数集合的内存艺术
当 Set 中元素全是整数且数量不多时,Redis 使用 intset 编码。它的特点是:有序 + 不重复 + 二分查找 + 紧凑存储。
c
// src/intset.h
typedef struct intset {
uint32_t encoding; // INTSET_ENC_INT16 / INT32 / INT64
uint32_t length; // 元素个数
int8_t contents[]; // 柔性数组,实际类型取决于 encoding
} intset;
contents 虽然声明为 int8_t[],但实际存储类型由 encoding 决定:
| encoding | 值范围 | 每元素字节 |
|---|---|---|
INTSET_ENC_INT16 |
-32768, 32767 | 2 字节 |
INTSET_ENC_INT32 |
-2\^31, 2\^31-1 | 4 字节 |
INTSET_ENC_INT64 |
-2\^63, 2\^63-1 | 8 字节 |
编码升级 :当插入一个超出当前范围的整数时,整个 intset 升级编码(如 INT16→INT32),但不支持降级。插入后 encoding 只升不降,即使大元素被删除了也不会降回去。
十、对象共享:0~9999 的整数池
Redis 启动时会预先创建 0 到 9999 共 10000 个整数的 redisObject,放在一个共享池中。当需要这些值的时候,直接复用,不再分配新内存。
c
// src/object.c
#define OBJ_SHARED_INTEGERS 10000
// 初始化
for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
shared.integers[j] = createObject(OBJ_STRING, (void*)(long)j);
shared.integers[j]->refcount = OBJ_SHARED_REFCOUNT; // 标记为不可释放
}
注意事项:
- 共享池只对 int 编码有效,其他编码对象不共享(因为比较成本高)
OBJ_SHARED_REFCOUNT是一个特殊值(INT_MAX),标记为"永不释放"- Redis 7.0+ 中,共享池的范围可以配置
shared-object-max-memory - 生产环境小 Tip:尽量用整数 ID 做 key,减少内存碎片
十一、内存优化实战建议
结合以上所有底层知识,总结几条立即可用的优化策略:
11.1 String 优化
bash
# ❌ 差:每次都是一个新对象
SET user:1001:name "zhangsan"
SET user:1001:age "28"
SET user:1001:city "beijing"
# ✅ 好:用 Hash 聚合,小对象走 listpack 编码,极省内存
HSET user:1001 name "zhangsan" age 28 city "beijing"
11.2 关注编码阈值
bash
# Hash 编码优化
# 确保 field 数量 < hash-max-listpack-entries (512)
# 确保每个 value < hash-max-listpack-value (64)
# 检查当前编码
OBJECT ENCODING user:1001 # 如果返回 "hashtable",说明已经退化了
DEBUG OBJECT user:1001 # 查看更详细的内存信息
# 如果退化了,可以手动重建
HSCAN user:1001 0 # 检查 field 数量
11.3 List 的压缩配置
bash
# 如果用于消息队列(只操作两端,中间是历史数据)
CONFIG SET list-compress-depth 2 # 两端各 2 个节点不压缩,中间全压缩
# 可以节省 30%~50% 内存
11.4 避免大 Key 导致连锁更新
bash
# ❌ 避免:单个 List 存储数百万元素
# 每个 quicklist 节点(listpack 节点)默认不超过 8KB
# 即使 100 万元素,也拆成了约 500 个节点,链表开销可控
# ✅ 关注每个元素的平均大小,评估内存
MEMORY USAGE mylist # Redis 4.0+ 查看 key 的内存占用
十二、总结
本文从源码层面拆解了 Redis 六大内部数据结构的设计原理:
| 数据结构 | 适用类型 | 核心特点 |
|---|---|---|
| SDS | String 底层 | 二进制安全、O(1) 长度、预分配+惰性释放、5 级头结构 |
| Ziplist → Listpack | Hash/List/ZSet 紧凑编码 | 极致省内存,listpack 彻底解决了连锁更新 |
| Quicklist | List | 双向链表+listpack,LZF 压缩+两端不压缩 |
| Dict | Hash/键空间 | 双表渐进式 rehash,SipHash 防攻击 |
| SkipList | ZSet 排序引擎 | span 字段支持 O(log N) 排名,实现简单 |
| Intset | Set 整数编码 | 有序数组+二分查找,无指针开销 |
核心收获 :Redis 的每种数据结构都不是孤立的------type 定义"是什么",encoding 决定"怎么存",两者解耦让 Redis 能在不同场景下自动选择最优存储方式。
如有疑问或指正,欢迎在评论区交流。