Redis底层数据结构全解密:从SDS到跳表,读懂“快”的基因
Redis底层数据结构全解密:从SDS到跳表,读懂“快”的基因
Redis 能以每秒十万级吞吐量处理请求,除了单线程和IO多路复用,更离不开精心设计的底层数据结构。String、List、Hash、Set、ZSet 五种对外数据类型,背后对应着 SDS、quicklist、dict、ziplist、intset、skiplist 等多种高效实现。本文深入源码与内存布局,图文并茂,带你彻底搞懂 Redis“快”的底层密码。
一、引言:外看五种类型,内看多种编码
当你使用 Redis 时,看到的是 String、List、Hash、Set、Sorted Set。但在内存中,Redis 会根据数据的大小、数量、内容,自动选择最合适的底层编码。这种“外松内紧”的设计,让 Redis 在内存效率与访问速度之间达到极致平衡。

二、String 的底层:SDS(简单动态字符串)
C 语言原生字符串以 \0 结尾,有三大痛点:获取长度 O(N)、缓冲区溢出风险、二进制不安全。Redis 自己实现了 SDS。
SDS 结构(以 sdshdr8 为例)
struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; // 已使用长度 uint8_t alloc; // 总分配长度 unsigned char flags; // 类型标识 char buf[]; // 柔性数组,真正存储字符串 };

SDS 核心优势
| 特性 | 说明 |
|---|---|
| O(1) 获取长度 | 直接读 len 字段 |
| 空间预分配 | 扩容时多分配空闲空间(alloc - len),减少内存重分配 |
| 惰性释放 | 缩短字符串时不立即回收,用 free 记录,便于后续追加 |
| 二进制安全 | 不以 \0 判断结束,可存储图片、序列化对象等 |
String 的三种内部编码
Redis 对 String 类型根据内容和长度采用不同编码:
| 编码 | 条件 | 说明 |
|---|---|---|
| int | 整数值且范围在 long 内 |
直接存储数值,不额外分配对象 |
| embstr | 字符串长度 ≤ 44 字节 | RedisObject 和 SDS 连续分配,一次内存操作 |
| raw | 字符串长度 > 44 字节 | RedisObject 和 SDS 分别分配,两次内存操作 |
为什么 embstr 只能读?
embstr 对象在修改时会先转为 raw,因为重新分配需要移动整个连续内存块。
> set num 100 > object encoding num "int" > set short "hello world" # 长度 11 ≤44 > object encoding short "embstr" > set long "aaaaaaaaaa... (45个a)" > object encoding long "raw"
三、List 的底层:quicklist(双向链表 + ziplist)
Redis 3.2 之前,List 使用 ziplist(元素少且短)或 linkedlist。3.2 之后统一为 quicklist:一个双向链表,每个节点是一个 ziplist。
quicklist 整体结构
typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; // 所有 ziplist 中的元素总数 unsigned long len; // quicklist 节点数 int fill; // 每个 ziplist 的最大大小(由配置决定) unsigned int compress; // 压缩深度 } quicklist;
每个 quicklistNode 包含一个 ziplist 指针,以及前后指针。

ziplist 内部结构
ziplist 是一块连续内存,存储了每个 entry 的前一个节点长度、当前节点编码和值,以空间换时间,内存非常紧凑。
<zlbytes> <zltail> <zllen> <entry1> <entry2> ... <zlend>
每个 entry 的编码会动态选择 1 字节、2 字节或 5 字节来存储长度,因此修改中间 entry 可能导致连锁更新(后续 entry 的 prevlen 长度变化),影响性能。故 ziplist 仅用于小数据量。
配置参数
list-max-ziplist-size -2 # 每个 ziplist 最大 8KB(-2 代表 8KB) list-compress-depth 0 # 不压缩
四、Hash 的底层:ziplist 与 dict
Hash 存储键值对,底层可使用 ziplist(小数据)或 hashtable(dict)(大数据)。
什么时候用 ziplist?
-
键值对数量 < 512(
hash-max-ziplist-entries) -
所有键和值的字符串长度 < 64 字节(
hash-max-ziplist-value)
在 ziplist 中,两个 entry 为一对(key 紧跟着 value),按插入顺序排列。
什么时候用 dict?
超过阈值后,自动转为 dict。dict 是 Redis 最核心的哈希表实现,采用链地址法解决冲突,支持渐进式 rehash。
dict 结构层次
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 的幂)
unsigned long sizemask;
unsigned long used; // 已有节点数
} dictht;
typedef struct dict {
dictht ht[2]; // ht[0] 主表,ht[1] 用于 rehash
long rehashidx; // -1 表示未 rehash
} dict;

渐进式 rehash
当负载因子过大(used/size > 5 且未在 bgrewriteaof)或强制扩容时,Redis 会:
-
为
ht[1]分配2*size的空间。 -
将
rehashidx设为 0,表示开始 rehash。 -
每次增、删、改、查操作时,顺带将
ht[0]中rehashidx索引的整条链迁移到ht[1],然后rehashidx++。 -
迁移完毕,交换
ht[0]和ht[1],rehashidx = -1。
这样把一次性 O(N) 操作分摊到多次请求中,避免服务卡顿。
五、Set 的底层:intset 与 dict
Set 存储不重复元素,根据元素类型和数量选择编码:
-
所有元素都是整数,且数量 ≤
set-max-intset-entries(默认 512)→ intset -
否则 → dict(value 为 NULL)
intset 结构
typedef struct intset { uint32_t encoding; // INTSET_ENC_INT16, INT32, INT64 uint32_t length; int8_t contents[]; // 按 encoding 存储实际整数 } intset;
intset 是一个有序数组,查找用二分查找(O(log N)),插入需要移动元素(O(N))。由于元素少,性能完全够用。

六、ZSet(有序集合)的底层:ziplist 与 skiplist+dict
ZSet 存储 member -> score 的映射,且按 score 排序。底层实现:
-
元素数量 ≤
zset-max-ziplist-entries(128)且 member 长度 ≤zset-max-ziplist-value(64)→ ziplist(按 score 升序存储 member 和 score 交替) -
否则 → skiplist + dict
跳表(skiplist)结构
跳表是一种随机化的多层链表,通过维护多级索引实现 O(log N) 的平均查找,且实现比红黑树简单。
typedef struct zskiplistNode { sds ele; 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; typedef struct zset { dict *dict; // member -> score 快速查找 zskiplist *zsl; // 按 score 排序的范围查询 } zset;
查找示例(查找分数 23):

从最高层开始,逐层下降,快速定位到接近目标的位置。
七、编码转换总结表
| 数据类型 | 小数据编码 | 大数据编码 | 默认阈值 | 配置参数 |
|---|---|---|---|---|
| String | int, embstr | raw | 44 字节 (embstr→raw) | 无 |
| List | quicklist(内含 ziplist) | quicklist | 统一 3.2+ | list-max-ziplist-size |
| Hash | ziplist | dict | 512 entries / 64 bytes | hash-max-ziplist-entries/value |
| Set | intset | dict | 512 个整数 | set-max-intset-entries |
| ZSet | ziplist | skiplist+dict | 128 entries / 64 bytes | zset-max-ziplist-entries/value |
所有编码转换都是单向的(小→大),不会自动回退,除非重新设置数据。
八、总结:Redis “快” 的三层基因
-
内存操作:所有数据在内存中,远离磁盘 I/O。
-
高效数据结构:SDS、ziplist、quicklist、dict、intset、skiplist 均针对 Redis 场景极致优化。
-
自适应编码:根据数据特征动态选择最省内存或最快的实现。
理解底层实现,你就能写出更高效的 Redis 使用方式,也能从容应对面试中的“String 为什么有两种编码”“ZSet 如何实现范围查询”等问题。
如果觉得本文对你有帮助,欢迎点赞、收藏、转发!
关注我,持续输出 Redis 内核与性能优化干货。
浙公网安备 33010602011771号