2. Redis核心数据结构与底层编码:String、List、Hash、Set、ZSet的底层实现
Redis之所以能实现微秒级读写,核心在于其底层数据结构并非简单的键值对,而是针对不同场景设计了多种高效编码。理解这些编码的切换机制与内存权衡,是进行热点削峰与性能调优的基础。
2.1 数据结构与底层编码总览
Redis对外暴露五种核心数据类型,每种类型在内部根据数据规模与特征,动态选择不同的底层编码(encoding)。下表总结了各类型的默认编码与切换条件:
| 数据类型 | 底层编码(按切换顺序) | 切换触发条件 | 核心优势 |
|---|---|---|---|
| String | int → embstr → raw | 整数用int;≤44字节用embstr;>44字节用raw | 整数运算原子性;短字符串零拷贝 |
| List | quicklist(3.2+) | 统一使用quicklist(ziplist + 双向链表混合) | 内存紧凑;两端操作O(1) |
| Hash | ziplist → hashtable | field数<512且每个value≤64字节时用ziplist | 小hash节省内存;大hash快速随机访问 |
| Set | intset → hashtable | 所有元素为整数且数量<512时用intset | 整数集合极省内存;成员检查O(1) |
| ZSet | ziplist → skiplist + dict | 元素数<128且每个member≤64字节时用ziplist | 小zset紧凑;大zset高效范围查询 |
调优要点: 在热点削峰场景中,应主动控制Hash/ZSet的field数量与value长度,使其尽量保持在ziplist编码范围内,可减少内存碎片并提升读写吞吐。
2.2 String底层编码:SDS与内存优化
String的底层并非C语言原生字符串,而是Redis自建的SDS(Simple Dynamic String)。SDS相比C字符串有三个关键改进:
- O(1)长度获取:SDS头中存储len属性,无需遍历。
- 预分配与惰性释放:扩容时额外分配free空间,减少内存重分配次数。
- 二进制安全:不以'\0'为结束符,可存储图片、序列化对象等二进制数据。
编码切换示例:
# 整数编码
SET key 100
OBJECT ENCODING key # 输出 "int"
# 短字符串(≤44字节)
SET key "hello"
OBJECT ENCODING key # 输出 "embstr"
# 长字符串(>44字节)
SET key "a...(45个字符)..."
OBJECT ENCODING key # 输出 "raw"
性能权衡: embstr编码只分配一次内存(SDS头+数据连续),而raw需要两次分配。在热点削峰中,应尽量将缓存value控制在44字节以内,利用embstr的CPU缓存友好特性。
2.3 List底层编码:从ziplist到quicklist
Redis 3.2版本之后,List统一使用quicklist编码。quicklist本质是双向链表 + ziplist的混合结构:每个链表节点内部是一个ziplist(压缩列表),节点之间通过双向指针连接。
关键配置参数:
list-max-ziplist-size:控制每个ziplist的最大大小(默认8KB),超过则分裂。list-compress-depth:控制两端不压缩的节点数(默认0不压缩),用于减少内存。
性能陷阱: 在热点削峰场景中,如果List频繁执行LTRIM或LPOP/RPOP操作,应避免ziplist过大导致连锁更新(cascade update)。建议将list-max-ziplist-size控制在4KB~8KB之间。
2.4 Hash与ZSet:ziplist vs 跳表
Hash和ZSet在数据量小时都使用ziplist编码,数据量增大后切换为hashtable或skiplist + dict。ziplist是一种连续内存块,通过固定偏移量存储数据,内存利用率极高,但插入/删除需要移动后续元素。
ZSet的skiplist(跳表)是Redis的核心创新之一:
- 每个节点包含多个层级(level),上层节点跨越更多元素,实现O(logN)的查找效率。
- 与平衡树相比,跳表实现简单、范围查询友好、并发插入无需复杂旋转。
- ZSet同时维护一个dict用于O(1)的成员分数查询。
2.5 Set底层编码:intset的极致压缩
当Set中所有元素都是整数且数量不超过set-max-intset-entries(默认512)时,Redis使用intset编码。intset是一个有序的整数数组,按升序排列,支持二分查找。
intset的内存优势:
- 每个整数只占用实际位数(16/32/64位自动升级),无指针开销。
- 相比hashtable,intset节省约70%~90%的内存。
热点削峰案例: 某电商秒杀场景使用Set存储已抢购用户ID(整数),利用intset编码将内存占用从200MB降至15MB,同时SISMEMBER操作仍保持O(logN)效率。
2.6 内存与性能权衡总结
| 编码类型 | 内存效率 | 读写性能 | 适用场景 |
|---|---|---|---|
| int | 极高(8字节/元素) | O(1)原子操作 | 计数器、自增ID |
| embstr | 高(一次分配) | O(N)但CPU缓存友好 | 短缓存value(≤44字节) |
| raw | 中(两次分配) | O(N)适合大对象 | JSON、序列化数据 |
| ziplist | 极高(连续内存) | O(N)但小数据量可忽略 | 小Hash、小ZSet、小List |
| quicklist | 高(ziplist+链表) | 两端O(1),中间O(N) | 消息队列、时间线 |
| intset | 极高(无指针) | O(logN)二分查找 | 整数集合、标签系统 |
| skiplist | 中(多级指针) | O(logN)范围查询 | 排行榜、延时队列 |
| hashtable | 中(哈希表+链表) | O(1)平均 | 大Hash、大Set |
调优红线: 在热点削峰场景中,切勿盲目追求内存压缩而忽略CPU开销。例如,将Hash的field数控制在512以内使用ziplist是好的,但如果频繁执行HGETALL遍历所有field,ziplist的O(N)遍历反而会成为瓶颈。此时应权衡是否提前切换为hashtable。