欢迎来到窥视未来的博客

https://github.com/lwx57280 https://gitee.com/li_VillageHead

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 在内存效率与访问速度之间达到极致平衡。

diagram-flowchart (4)

二、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[];       // 柔性数组,真正存储字符串
};

diagram-flowchart (5)

 

 

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 指针,以及前后指针。

 

image

 

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;

diagram-flowchart

 

渐进式 rehash

当负载因子过大(used/size > 5 且未在 bgrewriteaof)或强制扩容时,Redis 会:

  1. ht[1] 分配 2*size 的空间。

  2. rehashidx 设为 0,表示开始 rehash。

  3. 每次增、删、改、查操作时,顺带将 ht[0]rehashidx 索引的整条链迁移到 ht[1],然后 rehashidx++

  4. 迁移完毕,交换 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))。由于元素少,性能完全够用。

diagram-flowchart (1)

六、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):

diagram-flowchart (2)

从最高层开始,逐层下降,快速定位到接近目标的位置。

七、编码转换总结表

 
数据类型小数据编码大数据编码默认阈值配置参数
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 “快” 的三层基因

  1. 内存操作:所有数据在内存中,远离磁盘 I/O。

  2. 高效数据结构:SDS、ziplist、quicklist、dict、intset、skiplist 均针对 Redis 场景极致优化。

  3. 自适应编码:根据数据特征动态选择最省内存或最快的实现。

理解底层实现,你就能写出更高效的 Redis 使用方式,也能从容应对面试中的“String 为什么有两种编码”“ZSet 如何实现范围查询”等问题。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发
关注我,持续输出 Redis 内核与性能优化干货。

 

posted on 2026-06-13 20:13  k8s-Mango  阅读(2)  评论(0)    收藏  举报

导航