Redis 的高性能不仅源于其线程模型和 I/O 机制,更依赖于精心设计的数据存储结构 —— 从全局哈希表的快速查找,到针对不同数据类型优化的底层实现(如 SDS、ziplist、跳表),每一层设计都围绕 “高效” 和 “省内存” 展开。本文将拆解 Redis 的全局存储结构,再逐一解析 String、Hash、List、Set、Sorted-Set 的底层实现逻辑,帮你搞懂 Redis 数据存储的核心原理。
一、Redis 全局存储:哈希表的高效设计
Redis 用全局哈希表保存所有键值对,是实现 “Key 到 Value 快速访问” 的基础,核心特点如下:
1. 全局哈希表的结构
全局哈希表由 “哈希桶数组” 和 “dictEntry 节点” 组成:
- 哈希桶数组:数组的每个元素是一个指针,指向对应哈希桶中的 dictEntry 节点;
- dictEntry 节点:每个节点存储一组键值对,包含三个指针:key(指向键的指针)、value(指向值的指针,实际是 RedisObject)、next(解决哈希冲突的链表指针)。
2. 快速访问的原理
通过 Key 查找 Value 时,流程仅需两步:
- 计算 Key 的哈希值,用 “哈希值 & 哈希桶数组大小掩码” 得到哈希桶位置;
- 若哈希桶无冲突,直接访问 dictEntry 节点;若有冲突(多个 Key 哈希到同一桶),则遍历冲突链表找到目标 Key。
整个过程的时间复杂度接近 O (1),保证了 Redis 的快速查询能力。
3. RedisObject:统一的元数据管理
Redis 支持多种数据类型(String、Hash 等),不同类型需记录相同的元数据(如引用计数、过期时间)。因此 Redis 设计了RedisObject 结构体,作为所有 Value 的 “统一包装”:
- 结构组成:8 字节元数据(如数据类型、编码方式、引用计数)+ 8 字节指针(指向具体数据结构的内存地址);
- 作用:通过统一的元数据管理,简化不同数据类型的操作逻辑,同时支持 “同一数据类型切换不同底层编码”(如 String 可切换 int、embstr、raw 编码)。
二、各数据类型底层实现解析
Redis 针对不同数据类型的使用场景,设计了差异化的底层结构 —— 比如 String 追求快速读写,Hash 在 “小数据量” 时用 ziplist 省内存,Sorted-Set 用跳表实现有序查询。
1. String:灵活的多编码存储
String 是 Redis 最基础的数据类型,底层根据存储内容的不同,采用三种编码方式,兼顾 “速度” 和 “内存”:
(1)三种编码方式的适用场景
| 编码方式 | 适用场景 | 核心设计目标 |
|---|---|---|
| int | 存储 64 位有符号整数 | 省内存(无需额外指针) |
| embstr | 存储≤44 字节的字符串 | 连续内存,减少碎片 |
| raw | 存储>44 字节的字符串 | 独立空间,避免大内存拷贝 |
(2)关键结构:SDS(简单动态字符串)
当 String 存储字符串时(embstr/raw 编码),底层用SDS替代 C 语言字符串,解决了 C 字符串的四大痛点:
- SDS 的结构:包含三个字段:len(字符串长度,O (1) 获取)、free(未使用字节数,优化空间分配)、buf[](存储字符串的字节数组)。
- 相比 C 字符串的优势:
-
- 长度获取 O (1):C 字符串需遍历计数(O (N)),SDS 直接读len;
-
- 避免缓冲区溢出:SDS 修改前先检查len+free是否足够,不足则扩容;
-
- 优化空间分配:通过 “空间预分配”(扩容时多分配内存)减少重分配次数,“惰性空间释放”(缩短时不立即释放内存,留待后续使用);
-
- 二进制安全:C 字符串以 “\0” 为结束标识,无法存储二进制数据(如图片),SDS 用len判断结束,支持任意二进制数据。
(3)String 的实际内存占用
需注意:Redis 用 jemalloc 内存分配器,会按 “2 的幂次” 分配内存(如申请 24 字节实际给 32 字节),导致实际内存占用比 “数据本身” 大。例如存储一个 16 字节的整数:
- 内存组成:RedisObject(16 字节)+ dictEntry(32 字节)+ 整数数据(8 字节)= 56 字节(非精确值,因 jemalloc 分配规则略有差异)。
2. Hash:ziplist 与哈希表的动态切换
Hash 适合存储 “键值对集合”(如用户信息),底层根据数据量动态切换 “ziplist” 和 “哈希表”(Redis 的 dict 结构),平衡 “内存” 和 “查询效率”。
(1)编码切换的条件
当 Hash 同时满足以下两个条件时,用 ziplist 编码;否则切换为哈希表:
- hash-max-ziplist-entries ≤ 512:哈希表元素个数≤512;
- hash-max-ziplist-value ≤ 64:单个元素的 Key 和 Value 长度≤64 字节。
(2)两种底层结构的特点
- ziplist(压缩列表) :
-
- 结构:连续内存块,包含 header(头部)、多个 zipEntry(每个 Key 和 Value 各占一个 zipEntry)、end(尾部);
-
- 优点:内存连续,无指针开销,省内存;
-
- 缺点:查找需遍历(O (N)),修改元素可能引发内存重排(如元素变长导致后续元素位移)。
- 哈希表(dict) :
-
- 结构:与全局哈希表类似,由哈希桶数组和 dictEntry 链表组成;
-
- 优点:查找、修改效率高(O (1));
-
- 缺点:有指针开销,内存占用比 ziplist 大。
(3)哈希表的 rehash 优化
哈希表会因 “元素过多” 或 “元素过少” 触发 rehash(调整哈希桶数量),避免冲突链过长或内存浪费。Redis 用渐进式 rehash减少阻塞:
- 扩容触发:元素数 / 哈希桶大小 ≥ 1(或超过安全阈值 5),扩容为原大小的 2 倍;
- 缩容触发:元素数 / 哈希桶大小 ≤ 0.1,缩容为适合的大小;
- 渐进式 rehash 流程:
-
- 分配新哈希表(表 2),大小为表 1 的 2 倍;
-
- 每次处理客户端请求时,迁移表 1 的部分元素到表 2;
-
- 迁移完成后,释放表 1,用表 2 替代表 1;
-
- 迁移期间,查询 / 修改在表 1 和表 2 中进行,新增元素仅写入表 2。
3. List:ziplist 与双向链表的切换
List 适合存储 “有序可重复的元素序列”(如消息队列),底层同样根据数据量切换 “ziplist” 和 “双向链表(linkedlist)”。
(1)编码切换的条件
- 用 ziplist:元素长度≤64 字节且元素个数≤512;
- 用 linkedlist:不满足上述条件时切换。
(2)双向链表的结构
linkedlist 由 “链表头” 和 “listNode 节点” 组成:
- 链表头:记录表头、表尾、元素个数、节点操作函数(复制、释放、对比);
- listNode 节点:包含prev(前驱指针)、next(后继指针)、value(元素值,指向 RedisObject);
- 优点:插入、删除效率高(O (1),只需修改指针);
- 缺点:指针开销大,内存碎片化。
4. Set:intset 与哈希表的选择
Set 是 “无序不可重复的元素集合”(如好友列表),底层根据元素类型和数量,用 “intset(整数集合)” 或 “哈希表” 存储。
(1)编码切换的条件
- 用 intset:所有元素是整数且个数≤512;
- 用哈希表:不满足上述条件(如包含字符串元素或个数超 512)。
(2)两种结构的特点
- intset:
-
- 结构:连续内存块,包含 “编码方式(如 int16、int32)”、“元素个数”、“整数数组(有序存储)”;
-
- 优点:省内存,查找用二分法(O (logN));
-
- 缺点:仅支持整数,元素个数超限时需扩容(复制整个数组)。
- 哈希表:
-
- 结构:与全局哈希表类似,Value 固定为 NULL(仅用 Key 存储 Set 元素);
-
- 优点:支持任意类型元素,查找效率高(O (1));
-
- 缺点:内存占用比 intset 大。
5. Sorted-Set:ziplist 与跳表的高效结合
Sorted-Set 是 “有序不可重复的元素集合”(如排行榜),按元素的 “score” 排序,底层根据数据量切换 “ziplist” 和 “跳表(skiplist)+ 哈希表” 的组合。
(1)编码切换的条件
- 用 ziplist:元素个数≤128 且单个元素长度≤64 字节;
- 用跳表 + 哈希表:不满足上述条件。
(2)两种结构的特点
- ziplist:
-
- 存储方式:每个元素用两个相邻 zipEntry 存储(前一个存元素,后一个存 score),按 score 从小到大排序;
-
- 优点:省内存;
-
- 缺点:插入需先找位置(O (N)),效率低。
- 跳表 + 哈希表:
-
- 跳表(skiplist):负责按 score 有序存储和范围查询(如 “score≥10 且≤20 的元素”);
-
-
- 结构:由 “zskiplist(记录表头、表尾、最大层数、元素个数)” 和 “zskiplistNode(每个节点包含多层索引、后退指针、score、元素值)” 组成;
-
-
-
- 核心原理:用 “多层索引” 实现快速查找 —— 最底层是完整元素链表,上层索引是下层的 “稀疏采样”,查找时从高层索引往下跳,平均复杂度 O (logN);
-
-
- 哈希表:负责 “元素到 score” 的快速映射(O (1) 获取元素的 score);
-
- 优点:有序查询和单点查询效率都高;
-
- 缺点:跳表的多层索引会占用额外内存(空间换时间)。
三、总结:Redis 存储设计的核心思路
Redis 数据存储的每一层设计,都围绕 “场景适配” 和 “性能平衡” 展开:
- 全局层面:用哈希表保证 Key 的 O (1) 查找,用 RedisObject 统一元数据管理;
- 类型层面:针对不同数据类型的使用场景,设计差异化底层结构(如 String 用 SDS 解决 C 字符串痛点,Sorted-Set 用跳表实现有序查询);
- 动态优化:支持编码切换(如 Hash 的 ziplist→哈希表)和渐进式 rehash,平衡 “内存占用” 和 “操作效率”,避免单线程阻塞。
理解这些底层原理,能帮你在实际使用中做出更合理的选择 —— 比如用 Hash 存储小数据量用户信息以省内存,用 Sorted-Set 的跳表结构实现高效排行榜,真正发挥 Redis 的性能优势。