Files
autumn-recruitment/03.Redis/core/Redis 五大核心数据结构.md
T

5.9 KiB
Raw Blame History

tags, create time, update time
tags create time update time
redis
data-structures
sds
quicklist
skiplist
2026-08-08 18:43 2026-08-08 18:43

Redis 五大核心数据结构

概述

Redis 的性能基石在于其精心设计的底层数据结构。Redis 的每个值都有多种编码(encoding),会根据数据规模和内容自动切换,在内存占用和操作效率之间寻找最优解。理解这些编码的切换策略,是面试中的高频考点,也是性能调优的关键前提。

核心原理

String — SDS(Simple Dynamic String)

Redis 的 String 类型不是直接用 C 字符串,而是用 SDS(Simple Dynamic String)结构。SDS 的定义如下:

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 伪代码示意 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 编码切换逻辑非常直观:

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 对象的内部编码:

# Redis CLI 中查看键的内部编码
127.0.0.1:6379> OBJECT ENCODED mykey
"ziplist"   # 或 "skiplist", "hashtable", "intset"
// 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 替代。

关联笔记