【Redis】5个基本数据类型
2025/12/27...大约 5 分钟
Redis 提供 5 大基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(哈希)、Zset(有序集合)。其底层实现会根据数据特征智能切换数据结构,以达到性能与内存的最优平衡。
0、底层数据结构简介
- 压缩列表 (Ziplist):早期使用的紧凑结构,但因存储“前一项长度”而存在连锁更新风险,修改一个元素可能引发后续元素的内存重排。
- 列表包 (ListPack):Redis 7.0 引入,是 Ziplist 的优化版。它将“长度信息”放在条目末尾,且只记录自身长度,彻底解决了连锁更新问题。
- 哈希表 (HashTable):使用渐进式 rehash 进行扩容。内部维护两个哈希表(
ht[0]和ht[1]),扩容时逐步将ht[0]的数据迁移到更大的ht[1]。期间,新写入的数据直接进入ht[1],查找时会同时检查两个表。迁移完成后释放ht[0],并将ht[1]设置为新的主表。
一、String
String 的底层是 SDS (Simple Dynamic String)。SDS 有 5 种类型(sdshdr5/8/16/32/64),根据字符串长度自动选择,以优化内存。
struct sdshdr {
int len; // 字符串实际长度
int alloc; // 分配的总空间
char buf[]; // 存储数据的柔性数组
unsigned char flags; // 标识 SDS 类型
};核心特点:
- 二进制安全:不依赖
\0判断结尾,可存储任意二进制数据(如图片、序列化对象)。为了兼容 C 标准库函数,SDS 末尾仍会添加\0,但这不影响其二进制安全性。 - O(1) 获取长度:直接读取
len字段。 - 动态扩容:写入前检查剩余空间,不足时自动扩容,避免缓冲区溢出。
- 内存优化:对不同长度的字符串使用不同大小的头部,减少内存开销。
三种编码(自动切换):
- INT:存储整数值(如
SET key 123),直接将数值存储在指针位置,无需额外分配。 - EMBSTR:存储短字符串(≤ 44 字节)。Redis 对象头与 SDS 数据存储在同一块连续内存中,内存局部性好,一次分配完成。
- RAW:存储长字符串(> 44 字节)。Redis 对象头与 SDS 数据分开分配。
44 字节边界是根据 64 位系统内存对齐(jemalloc 分配器常用 64 字节块)精确计算得出的优化值。
二、List
List 的底层是 QuickList,它是一个宏观上的双向链表,但每个链表节点 (quicklistNode) 内部并不直接存储数据,而是包含一个 ListPack 来紧凑存储多个元素。
QuickList 结构:
[quicklistNode] <-> [quicklistNode] <-> [quicklistNode]
↓ ↓ ↓
ListPack ListPack ListPack
[e1,e2,e3] [e4,e5,e6] [e7,e8,e9]设计优势:
- 对比纯链表:避免了每个元素都需要
prev/next指针的巨大内存开销。 - 对比纯 ListPack:避免了在超长连续内存中插入/删除元素时的大规模数据移动。
通过配置 list-max-listpack-size 可以控制每个 ListPack 的最大容量(如 -2 表示约 8KB),从而在内存紧凑性和节点内操作性能之间取得平衡。
三、Set
Set 存储无序且唯一的元素,底层使用 IntSet 或 HashTable。
- IntSet:当 所有元素都是整数 且 元素数量 ≤
set-max-intset-entries(默认 512) 时使用。它是一个有序整数数组,支持二分查找(O(log N)),内存极其紧凑。 - HashTable:当插入非整数元素或元素数量超过阈值时,自动转换为 HashTable。此时只使用字典的键(Key)来存储元素,值(Value)统一为
NULL,实现 O(1) 复杂度的查找。
关键点:
- IntSet 升级:当新插入的整数超出当前编码范围(如从 int16 到 int32)时,会触发升级,重新分配内存并转换所有元素。升级是单向不可逆的。
- HashTable 也使用渐进式 rehash,扩容机制与其他场景下的哈希表一致。
四、ZSet (Sorted Set)
ZSet 存储带分值(score)的有序唯一元素,底层使用 ListPack 或 字典(Dict) + 跳表(SkipList) 的双结构。
- ListPack:当 元素数量 ≤
zset-max-listpack-entries(默认 128) 且 每个元素值长度 ≤zset-max-listpack-value(默认 64 字节) 时使用。元素按分值连续紧凑存储。 - Dict + SkipList:当不满足上述条件时使用。
- 跳表 (SkipList):维护分值有序结构,支持 O(log N) 的范围查询(如
ZRANGE,ZRANK)。 - 字典 (Dict):实现 O(1) 复杂度的单点查询(如
ZSCORE)。
- 跳表 (SkipList):维护分值有序结构,支持 O(log N) 的范围查询(如
双结构设计精髓:
- 空间换时间:用额外空间(指针)同时获得 O(1) 查找和 O(log N) 范围查询。
- 数据共享:两个结构中的元素值(SDS)指向同一内存对象,避免重复存储。
五、Hash
Hash 存储字段-值对(field-value),底层使用 ListPack 或 HashTable。
- ListPack:当 字段数量 ≤
hash-max-listpack-entries(默认 512) 且 所有字段值长度 ≤hash-max-listpack-value(默认 64 字节) 时使用。字段和值交替存储在 ListPack 中。 - HashTable:当不满足上述条件时使用,提供 O(1) 的字段操作。
为什么小 Hash 还用 ListPack?
虽然 ListPack 的查询是 O(N),但对于小规模数据(几十个字段):
- 内存效率极高:比 HashTable 节省约 30%-50% 内存(无指针和额外元数据开销)。
- 缓存友好:数据连续存储,CPU 缓存命中率高。
- 自动转换:超过阈值后 Redis 会自动转为 HashTable,对应用透明。
这是一种典型的 “二八定律”优化:为80%的小数据场景优化内存,20%的大数据场景则保证性能。