--- tags: [redis, data-structures, sds, quicklist, skiplist] create time: 2026-08-08 18:43 update time: 2026-08-08 18:43 --- # Redis 五大核心数据结构 ## 概述 Redis 的性能基石在于其精心设计的底层数据结构。Redis 的每个值都有多种编码(encoding),会根据数据规模和内容自动切换,在内存占用和操作效率之间寻找最优解。理解这些编码的切换策略,是面试中的高频考点,也是性能调优的关键前提。 ## 核心原理 ### String — SDS(Simple Dynamic String) Redis 的 String 类型不是直接用 C 字符串,而是用 SDS(Simple Dynamic String)结构。SDS 的定义如下: ```c struct sdshdr { int len; // buf 中已使用的字节数 int free; // buf 中未使用的字节数 unsigned char flags; // 低3位标记类型:SDS_TYPE_5/8/10/16/32 char buf[]; // 可变长度字符数组,以 \0 结尾 }; ``` > [!NOTE] > SDS 有 5 种格式(SDS_TYPE_5 到 SDS_TYPE_32),通过 `flags` 字段的低 3 位区分。SDS_TYPE_5 用于短字符串(len < 32),直接内嵌在 dictEntry 中而不分配独立对象。 **为什么不用 C 字符串?** | 问题 | C 字符串 | SDS | |------|---------|-----| | O(N) 获取长度 | strlen 遍历计数 | len 字段 O(1) | | 缓冲区溢出风险 | strcpy/strcat 不安全 | 分配时检查空间 | | 修改需重新分配 | 每次可能 realloc | 惰性空间释放(free 保留) | | 二进制安全 | 遇 \0 截断 | 记录 len,可存任意二进制数据 | **innative 优化**:从 Redis 7.0 开始,小字符串使用 SDS_TYPE_5 直接存储在 dictEntry 的键槽中,完全避免额外的堆分配,减少内存碎片和 malloc 开销。 ### List — quicklist Redis 3.2 之前用 ziplist + linkedlist 实现 List,3.2 之后统一为 **quicklist**。 quicklist 是一个双向链表,每个节点(quicklistNode)是一个 ziplist 或 listpack(Redis 7.0+)。通过 `list-max-ziplist-size` 控制每个节点的压缩列表大小: | list-max-ziplist-size 值 | 含义 | |--------------------------|------| | 正数 N | 该节点最多 N 个元素 | | -1 | 每节点不限(仅受 memory 约束) | | -2 | 每节点 <= 8KB | | -3 | 每节点 <= 4KB | | -4 | 每节点 <= 2KB | | -5 | 每节点 <= 1KB(默认) | ```go // Go 伪代码示意 quicklist 的结构 type quicklist struct { head *quicklistNode tail *quicklistNode count int64 // 总元素数 zipsize int // list-max-ziplist-size 全局配置 } type quicklistNode struct { prev *quicklistNode next *quicklistNode ptr unsafe.Pointer // 指向 ziplist/listpack sz uint32 // 字节大小 } ``` ### Hash — zipmap / hashtable 切换 Hash 有两种编码,根据数据和元素数量自动切换: - **zipmap**(Redis 4.0 之前)→ 当元素数量 >= 512 且最大 value 长度 < 64 字节时使用 hashtable - **ziplist**(Redis 4.0 ~ 5.x)→ 元素数量 <= 512 且所有 value 长度 < 64 字节 - **hashtable**(Redis 4.0+ 默认)→ 只要有一个 value > 64 字节 或 元素数 > 512 就切换到 hashtables > [!WARNING] > Redis 4.0+ 废弃了 ziplist 作为 Hash 编码,默认始终使用 hashtable。这是因为 ziplist 插入删除的均摊代价在大数据量下反而更高。ziplist 编码目前仅在 ZSet 场景中保留。 ### Set — intset → hashtable 切换 Set 编码切换逻辑非常直观: ```mermaid graph TD A["Set 创建"] --> B{"所有元素都是整数?"} B -->|是| C["intset 编码"] B -->|否| D["hashtable 编码"] C --> E{"新加入的元素仍是整数?"} E -->|是| F["继续 intset"] E -->|否| G["转换为 hashtable"] D --> H["维持 hashtable"] ``` - **intset**:有序整数集合,紧凑排列在连续内存中。支持 int16_t、int32_t、int64_t 三种格式,升级时 realloc 整个结构(不可降级)。 - **hashtable**:通用哈希表,任何类型的成员都能存储。 ### ZSet — skiplist + ziplist / quicklist 切换 ZSet 是最复杂的结构,组合了两层编码: - **ziplist**(Redis 5.0 之前)/ **quicklist**(Redis 5.0+):当元素少且值小时用压缩列表存储 - **skiplist + hashtable**:标准模式,跳表按 score 排序,hashtable 按 member 做 O(1) 查找 > [!TIP] > 面试常考:跳表的平均查找复杂度 O(log N),最坏 O(N);Redis 跳表通过限制随机层级上限(maxlevel=32)和控制概率 p=0.25,保证最坏情况也在可接受范围。跳表节点包含:score(分数)、member(成员)、back(后退指针)、forward(多层前进指针数组)。 ## 代码示例 以下展示如何查看 Redis 对象的内部编码: ```bash # Redis CLI 中查看键的内部编码 127.0.0.1:6379> OBJECT ENCODED mykey "ziplist" # 或 "skiplist", "hashtable", "intset" ``` ```go // Go 中使用 Redigo 库观察 Redis 行为 import "github.com/gomodule/redigo/redis" conn, _ := redis.Dial("tcp", "localhost:6379") defer conn.Close() // HSET 少量数据 → ziplist/hashmap conn.Do("HSET", "user:1", "name", "alice", "age", "25") // 数据量大后自动切 hashtable —— 用户无感知 ``` ## 实践场景 1. **Hash 内存优化**:对于一个用户有几十个字段的小 Hash,手动合并成单个 Key(如 `user:1:name`)不如用一个 Hash 结构 `user:1`,因为 ziplist 能省大量内存。 2. **ZSet 排行榜实现**:电商商品销量排名、社交媒体点赞排行等场景天然适合 ZSet。`ZREVRANGEBYSCORE` 可以高效取 Top-N。 3. **List 替代方案**:如果 List 只用两端操作(LPUSH + RPOP),本质上是个队列;但 quicklist 的中间节点插入在大数据量时会有 O(N) 成本,考虑用 Stream 替代。 ## 关联笔记 - [[03.Redis/core/RDB 与 AOF 持久化]] - [[03.Redis/core/集群与哨兵机制]] - [[03.Redis/strategies/多级缓存架构设计]] - [[03.Redis/strategies/缓存穿透击穿雪崩解决方案]]