当我们使用 SET user:1001 "Alice" 或 HSET product:123 name "Phone" price 999 时,Redis 内部究竟发生了什么?数据是如何被存储、查找和修改的?为什么 Redis 能如此之快?
答案就藏在其精心设计的底层数据结构 之中。Redis 并非简单地为每种数据类型(String, Hash, List 等)绑定一种固定的数据结构,而是采用了一种自适应、多态的策略。它会根据数据的规模、类型和内容,动态选择最合适的内部编码(encoding),以在内存占用和 CPU 性能之间取得最佳平衡。
本文将带你深入 Redis 7 的底层世界,逐一解析其核心数据类型的内部实现。
一、统一的对象模型:redisObject
在 Redis 内部,所有的值(value)都被封装在一个名为 redisObject 的结构体中:
c
编辑
1structredisObject{2unsigned type:4;// 对象的上层类型 (string, list, hash...)3unsigned encoding:4;// 对象的底层编码方式4int refcount;// 引用计数5void*ptr;// 指向底层数据结构的指针6};
type字段决定了我们看到的数据类型(如string,hash)。encoding字段则揭示了其真正的底层存储格式(如embstr,hashtable,quicklist)。ptr指针指向实际存储数据的内存区域。
正是这种设计,使得 Redis 能够为同一种上层类型提供多种底层实现。
二、String 类型的三种面孔
String 是最基础的类型,但其底层却有三种不同的编码:
- **
int**:当字符串内容是一个可以表示为long类型的整数时,Redis 会直接将数值存储在ptr指针中(利用了指针的低几位来区分),省去了额外的内存分配。 - **
embstr(Embedded String)**:对于长度小于等于 44 字节 的短字符串,Redis 会将redisObject和其底层的 SDS(Simple Dynamic String)分配在同一块连续的内存中。这减少了内存分配次数,并提高了缓存局部性,读取速度更快。 - **
raw**:对于长度大于 44 字节的长字符串,redisObject和 SDS 会分开分配内存,ptr指向 SDS 结构。
SDS (简单动态字符串) 是 Redis 自己实现的字符串结构,它通过记录字符串长度 (
len) 避免了 C 字符串遍历计算长度的 O(N) 开销,并且是二进制安全的。
三、Hash, List, Set, ZSet 的底层演进:告别 ziplist,拥抱 listpack
这是 Redis 7 相比 Redis 6 最重大的变化 。过去广泛使用的 ziplist(压缩列表)因其连锁更新 问题(一个节点的扩容可能引发后续所有节点的连锁调整)而被性能更优、结构更稳定的 listpack 所取代。
Hash (哈希)
- 小对象 :当 Hash 中的键值对数量较少(默认
< 512)且每个值的长度较短(默认< 64字节)时,底层使用listpack存储。listpack将键和值紧凑地排列在一起,非常节省内存。 - 大对象 :一旦超出阈值,Redis 会将其转换为 **
hashtable**(哈希表),以保证 O(1) 的查询性能。注意:这个转换是单向的,一旦升级为 hashtable,就不会再降级回 listpack。
List (列表)
- 小列表 :对于元素较少的列表,底层直接使用 **
listpack**。 - 大列表 :对于元素较多的列表,Redis 使用 **
quicklist。quicklist本质上是一个 双向链表,但它的每个节点(quicklistNode)不再指向单个元素,而是指向一个 listpack**。这种设计结合了链表的灵活性(O(1) 头尾插入/删除)和listpack的内存紧凑性,是工程上的完美折中。
Set (集合)
- 整数集合 :如果 Set 中的所有元素都是整数,且数量不多(默认
< 512),则使用 **intset**(整数集合)存储,这是一个有序的整数数组,支持二分查找。 - 普通集合 :对于包含字符串或数量较多的集合,则使用
hashtable来保证 O(1) 的查找、插入和删除性能。
ZSet (有序集合)
- 小有序集 :当元素数量较少(默认
< 128)且成员值较短(默认< 64字节)时,使用listpack存储。成员和分数(score)会被打包在一起。 - 大有序集 :超出阈值后,会转换为 **
skiplist**(跳跃表)+hashtable的组合。skiplist用于维护元素的有序性(支持 O(log N) 的范围查询),而hashtable则用于提供 O(1) 的单点查询(通过 member 查 score)。
四、总结:Redis 7 底层数据结构全景图
表格
| 上层数据类型 | Redis 7 底层编码 (encoding) |
|---|---|
| String | int,embstr,raw |
| Hash | listpack,hashtable |
| List | listpack,quicklist(内含 listpack) |
| Set | intset,listpack,hashtable |
| ZSet | listpack,skiplist(+hashtable for dict) |
核心思想 :小而美,大而强 。对于小规模数据,优先选择内存紧凑的线性结构(如 listpack, intset);一旦数据规模增长到影响性能的临界点,便果断切换到为大规模数据优化的复杂结构(如 hashtable, skiplist)。
理解这些底层原理,不仅能帮助我们在面试中脱颖而出,更能指导我们在实际项目中做出更明智的数据建模决策,从而充分发挥 Redis 的极致性能。