2.Redis 数据存储原理:从全局结构到各类型底层实现

180 阅读9分钟

Redis 的高性能不仅源于其线程模型和 I/O 机制,更依赖于精心设计的数据存储结构 —— 从全局哈希表的快速查找,到针对不同数据类型优化的底层实现(如 SDS、ziplist、跳表),每一层设计都围绕 “高效” 和 “省内存” 展开。本文将拆解 Redis 的全局存储结构,再逐一解析 String、Hash、List、Set、Sorted-Set 的底层实现逻辑,帮你搞懂 Redis 数据存储的核心原理。

一、Redis 全局存储:哈希表的高效设计

Redis 用全局哈希表保存所有键值对,是实现 “Key 到 Value 快速访问” 的基础,核心特点如下:

image.png

1. 全局哈希表的结构

全局哈希表由 “哈希桶数组” 和 “dictEntry 节点” 组成:

  • 哈希桶数组:数组的每个元素是一个指针,指向对应哈希桶中的 dictEntry 节点;
  • dictEntry 节点:每个节点存储一组键值对,包含三个指针:key(指向键的指针)、value(指向值的指针,实际是 RedisObject)、next(解决哈希冲突的链表指针)。

2. 快速访问的原理

通过 Key 查找 Value 时,流程仅需两步:

  1. 计算 Key 的哈希值,用 “哈希值 & 哈希桶数组大小掩码” 得到哈希桶位置;
  1. 若哈希桶无冲突,直接访问 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 用跳表实现有序查询。

image.png

1. String:灵活的多编码存储

String 是 Redis 最基础的数据类型,底层根据存储内容的不同,采用三种编码方式,兼顾 “速度” 和 “内存”:

(1)三种编码方式的适用场景

编码方式适用场景核心设计目标
int存储 64 位有符号整数省内存(无需额外指针)
embstr存储≤44 字节的字符串连续内存,减少碎片
raw存储>44 字节的字符串独立空间,避免大内存拷贝

image.png

(2)关键结构:SDS(简单动态字符串)

当 String 存储字符串时(embstr/raw 编码),底层用SDS替代 C 语言字符串,解决了 C 字符串的四大痛点:

  • SDS 的结构:包含三个字段:len(字符串长度,O (1) 获取)、free(未使用字节数,优化空间分配)、buf[](存储字符串的字节数组)。
  • 相比 C 字符串的优势
    1. 长度获取 O (1):C 字符串需遍历计数(O (N)),SDS 直接读len;
    1. 避免缓冲区溢出:SDS 修改前先检查len+free是否足够,不足则扩容;
    1. 优化空间分配:通过 “空间预分配”(扩容时多分配内存)减少重分配次数,“惰性空间释放”(缩短时不立即释放内存,留待后续使用);
    1. 二进制安全: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 流程
    1. 分配新哈希表(表 2),大小为表 1 的 2 倍;
    1. 每次处理客户端请求时,迁移表 1 的部分元素到表 2;
    1. 迁移完成后,释放表 1,用表 2 替代表 1;
    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)),效率低。
  • 跳表 + 哈希表

image.png

    • 跳表(skiplist):负责按 score 有序存储和范围查询(如 “score≥10 且≤20 的元素”);
      • 结构:由 “zskiplist(记录表头、表尾、最大层数、元素个数)” 和 “zskiplistNode(每个节点包含多层索引、后退指针、score、元素值)” 组成;
      • 核心原理:用 “多层索引” 实现快速查找 —— 最底层是完整元素链表,上层索引是下层的 “稀疏采样”,查找时从高层索引往下跳,平均复杂度 O (logN);
    • 哈希表:负责 “元素到 score” 的快速映射(O (1) 获取元素的 score);
    • 优点:有序查询和单点查询效率都高;
    • 缺点:跳表的多层索引会占用额外内存(空间换时间)。

三、总结:Redis 存储设计的核心思路

Redis 数据存储的每一层设计,都围绕 “场景适配” 和 “性能平衡” 展开:

  1. 全局层面:用哈希表保证 Key 的 O (1) 查找,用 RedisObject 统一元数据管理;
  1. 类型层面:针对不同数据类型的使用场景,设计差异化底层结构(如 String 用 SDS 解决 C 字符串痛点,Sorted-Set 用跳表实现有序查询);
  1. 动态优化:支持编码切换(如 Hash 的 ziplist→哈希表)和渐进式 rehash,平衡 “内存占用” 和 “操作效率”,避免单线程阻塞。

理解这些底层原理,能帮你在实际使用中做出更合理的选择 —— 比如用 Hash 存储小数据量用户信息以省内存,用 Sorted-Set 的跳表结构实现高效排行榜,真正发挥 Redis 的性能优势。