一、Redis对象类型概述
1.1 Redis数据类型总览
Redis提供了丰富的数据类型,用于不同的业务场景:
| 对象类型 | 说明 | 典型场景 |
|---|---|---|
| String | 字符串 | 缓存、计数器、分布式锁 |
| List | 双向链表 | 队列、消息队列、最新列表 |
| Hash | 哈希表 | 存储对象、购物车 |
| Set | 无序集合 | 好友关系、抽奖 |
| Zset | 有序集合 | 排行榜 |
| Stream | 流数据类型 | 消息队列、事件流 |
| HyperLogLog | 基数统计 | UV统计 |
二、String类型详解
2.1 三种底层编码
查看命令:OBJECT ENCODING key
String类型根据数据特性分为三种底层实现:
+-----------------------------------------------+
| String 类型 |
+-----------------------------------------------+
| |
| int raw embstr |
| | | | |
| | | | |
| v v v |
| 整数 长字符串 短字符串 |
| 20字节以内 大于44字节 44字节以内 |
+-----------------------------------------------+
2.2 int编码
触发条件:
- 字符串长度小于等于20字节
- 内容是纯整数
示例:
SET num 12345678901234567890
OBJECT ENCODING num
-> "int"
存储形式:直接以long型整数存储,节省内存
特点:
- 直接存储为C语言的long类型
- 最高效的存储方式
- INCR/INCRBY等命令直接在整数上操作
2.3 raw编码
触发条件:
- 字符串长度大于44字节
示例:
SET article "这是一段很长的文章内容..."
(长度超过44字节)
OBJECT ENCODING article
-> "raw"
存储形式:SDS(Simple Dynamic String)动态字符串
SDS结构:
struct sdshdr {
int len; // 已用长度
int free; // 剩余空间
char buf[]; // 实际存储字符的数组
}
为什么是44字节?
embstr和raw的分界线是44字节,原因:
- embstr只分配一次内存(header + data连续)
- raw需要分配两次内存(header和data分开)
- 44字节时embstr刚好能用一次内存分配
- 超过44字节,embstr反而浪费内存
2.4 embstr编码
触发条件:
- 字符串长度小于等于44字节
示例:
SET name "zhangsan"
OBJECT ENCODING name
-> "embstr"
存储形式:嵌入式字符串,一次内存分配
内存布局对比:
raw编码(两次内存分配):
+--------+-------------+
| sdshdr | SDS头 |
+--------+-------------+
| char[] | 实际字符串 | <- 另一块内存
+--------+-------------+
embstr编码(一次内存分配):
+---------------------+
| redisObject | sdshdr |
+---------------+-------------+
| | char[] |
| 整体连续 | 实际字符串 |
+---------------+-------------+
embstr的优势:
- 只需一次内存分配,效率更高
- 数据连续存储,缓存友好
- 适合短字符串存储
三、List类型详解
3.1 quicklist双向链表
List的底层实现是quicklist:
本质上是双向链表
每个节点是ziplist(压缩列表)
兼顾快速访问和紧凑存储
quicklist结构:
+---------+---------+---------+---------+---------+
| head | ... | node | ... | tail |
| pointer| | +----+ | | pointer |
+---------+---------+--+----+-+---------+---------+
|
v
+---------------+
| ziplist |
| (压缩列表) |
+---------------+
3.2 ziplist压缩列表
ziplist的特点:
- 连续内存块,节省空间
- 每个节点记录前一个节点长度,支持双向遍历
- 内存布局紧凑
ziplist结构:
+--------+---------+---+-------+---+-------+---+----+
|zlbytes |zltail |zllen| entry1 |...| entryN |zlend|
|总长度 |末元素偏移|数量| 内容 |...| 内容 |0xFF|
+--------+---------+---+-------+---+-------+---+----+
节点entry结构:
+---------+--------------+--------+
| previous| content | encoding |
| length | (数据) | (编码) |
+---------+--------------+--------+
|
└── 记录前一个节点长度,支持从后向前遍历
压缩列表的局限:
- 新增/修改需要移动后续元素
- 节点过长时效率下降
四、Hash类型详解
4.1 两种底层编码
Hash类型根据数据量自动选择编码:
+---------------------------------------------+
| Hash 类型 |
+---------------------------------------------+
| |
| 元素数量 <= 512 |
| 且字段长度 <= 64字节 |
| | |
| v |
| ziplist(压缩列表) |
| (优化存储空间) |
| | |
| | 超过任一条件 |
| v |
| dict(哈希表) |
| (优化查询性能) |
+---------------------------------------------+
4.2 ziplist编码
触发条件:
- 节点数量小于等于512
- 每个字段字符串长度小于等于64字节
存储结构:
key: "***"
+--------+---------+---+-------+---+-------+---+----+
| field1 | value1 |...|field2 |...|field3 |...|zlend|
| "name" | "zhang" | | "age" | |"city" | |0xFF |
+--------+---------+---+-------+---+-------+---+----+
ziplist存储Hash的特点:
- 字段和值交替存储
- 新增字段到列表头部
- 节省内存,但查询需要遍历
4.3 dict编码
触发条件:
- 节点数量大于512
- 或字段字符串长度大于64字节
dict结构(哈希表):
+-----------------------------------------+
| dict结构 |
+-----------------------------------------+
| hashTable[] |
| +--------+--------+--------+--------+ |
| | slot 0 | slot 1 | slot 2 | slot 3 | |
| +--------+--------+--------+--------+ |
| | |
| v |
| +----------------+ |
| | dictEntry | |
| | key -> value | |
| +----------------+ |
+-----------------------------------------+
dict的特点:
- O(1)时间复杂度查询
- 内存开销比ziplist大
- 适合大量字段的场景
五、Set类型详解
5.1 两种底层编码
Set类型根据元素特性选择编码:
+---------------------------------------------+
| Set 类型 |
+---------------------------------------------+
| |
| 元素都是整数 |
| 且元素数量 <= 512 |
| | |
| v |
| intset(整数集合) |
| | |
| | 不满足 |
| v |
| dict(字典) |
+---------------------------------------------+
5.2 intset整数集合
触发条件:
- 所有元素都是整数
- 元素数量不超过512个
intset结构:
+--------+--------+--------+----------------+
|encoding|length | [] | 整数数组 |
| INT16 | 5 |0 3 5 7 | 按升序排列 |
+--------+--------+--------+----------------+
特点:
- 使用紧凑的整数数组存储
- 节省内存
- 支持二分查找
5.3 dict编码
触发条件:
- 元素包含字符串
- 或元素数量超过512
dict实现Set:
+-----------------------------------------+
| dict实现Set |
+-----------------------------------------+
| key = *** |
| value = NULL(只用key) |
| |
| 特性: |
| 1. 元素唯一性:key不重复 |
| 2. 元素无序:通过hash保证唯一 |
| 3. 快速查找:O(1)时间复杂度 |
+-----------------------------------------+
Set特有操作(差集、交集、并集)的实现:
SINTER setA setB
|
+-- 遍历setA的dict
+-- 检查元素是否在setB的dict中
+-- 返回都存在的元素
SDIFF setA setB
|
+-- 遍历setA的dict
+-- 检查元素是否不在setB中
+-- 返回setA有setB没有的元素
六、Zset类型详解
6.1 两种底层编码
Zset根据数据量选择编码:
+---------------------------------------------+
| Zset 类型 |
+---------------------------------------------+
| |
| 元素数量 <= 128 |
| 且member长度 <= 64字节 |
| | |
| v |
| ziplist(压缩列表) |
| (每个元素同时存储member和score) |
| | |
| | 超过任一条件 |
| v |
| skiplist + dict |
| (跳表排序 + 字典快速查找) |
+---------------------------------------------+
6.2 ziplist编码
触发条件:
- 元素数量小于等于128
- 每个member长度小于等于64字节
存储结构:
+---------+---------+---+-------------+---+-------------+---+----+
| member1 | score1 |...| member2 |...| member3 |...|zlend|
| "alice" | 850 | | "bob" | | "carol" | |0xFF |
+---------+---------+---+-------------+---+-------------+---+----+
score按从小到大排列
特点:
- member和score交替存储
- 按score排序
- 节省内存
6.3 skiplist跳表编码
触发条件:
- 元素数量大于128
- 或member长度大于64字节
跳表结构:
Level 3: [head] ----------------------> [node: carol]
Level 2: [head] ----> [node: bob] ----> [node: carol]
Level 1: [head] ----> [node: alice] --> [node: carol]
Level 0: [head] ----> [node: alice] --> [node: bob] -----> [node: carol]
跳跃:alice(850) -> bob(950) -> carol(1200)
跳表实现原理:
跳表是一种多级索引的结构:
Level 0: 包含所有元素(有序)
Level 1: 每隔一个元素选一个
Level 2: 每隔两个元素选一个
...
查找:从最高层开始,逐层向下逼近
1. 从Level 3开始,发现alice < target,继续前进
2. 到达bob < target,继续前进
3. carol > target,下降到Level 2
4. ... 逐步定位到目标位置
时间复杂度:
- 查找:O(log n)
- 插入:O(log n)
- 删除:O(log n)
空间复杂度:
- 每层节点数递减:n/2 + n/4 + n/8 + ... = 2n
- 空间复杂度O(n)
为什么Zset需要skiplist + dict?
Zset需要支持两种操作:
1. 按排名查找:ZRANGE 0 9 -> 需要跳表
2. 按member查score:ZSCORE -> 需要dict
单一数据结构的不足:
- 纯跳表:无法O(1)查member
- 纯dict:无法按排名有序
解决方案:skiplist + dict组合
+-----------------------------------------+
| Zset同时维护两个结构: |
| |
| 跳表:member -> score (用于排序) |
| 字典:member -> score (用于快速查找) |
| |
| 更新时同时维护两个结构 |
| 空间换时间思想 |
+-----------------------------------------+
内存布局:
+-----------------------------------------------+
| skiplist节点 |
+-----------------------------------------------+
| dictEntry (key=*** value=score) |
| +--------------------------------------------+
| | struct zset { | |
| | dict *dict; // member -> score | |
| | skiplist *zsl; // 排序用 | |
| | }; | |
| +--------------------------------------------+
+-----------------------------------------------+
dict和skiplist共享member字符串,score在两处都存储
七、编码转换规则汇总
7.1 String类型转换
| 条件 | 编码 | 说明 |
|---|---|---|
| 长度<=20且为整数 | int | 整数存储,最省空间 |
| 长度<=44 | embstr | 一次内存分配,高效 |
| 长度>44 | raw | SDS动态字符串 |
7.2 List类型转换
| 条件 | 编码 |
|---|---|
| 默认 | quicklist(ziplist节点) |
7.3 Hash类型转换
| 条件 | 编码 | 说明 |
|---|---|---|
| 元素<=512 且 字段<=64字节 | ziplist | 节省空间 |
| 元素>512 或 字段>64字节 | dict | 优化查询性能 |
7.4 Set类型转换
| 条件 | 编码 | 说明 |
|---|---|---|
| 全整数且元素<=512 | intset | 紧凑存储 |
| 其他情况 | dict | 支持任意类型 |
7.5 Zset类型转换
| 条件 | 编码 | 说明 |
|---|---|---|
| 元素<=128 且 member<=64字节 | ziplist | 节省空间 |
| 元素>128 或 member>64字节 | skiplist+dict | 兼顾排序和查找 |
八、Redis数据结构设计思想
8.1 为什么需要动态选择编码?
内存与性能的权衡:
Redis是内存数据库:
- 内存是稀缺资源
- 需要尽可能节省内存占用
单线程+事件循环:
- 需要快速响应命令
- 操作延迟直接影响性能
两种优化策略:
策略1:空间优化(节点少时)
+-- ziplist:紧凑连续存储,节省内存
但插入删除需要移动元素,O(n)
策略2:性能优化(节点多时)
+-- dict/skiplist:指针跳转,O(1)或O(log n)
但需要额外内存存储索引
Redis的选择:
数据少时,用空间换时间(ziplist)
数据多时,用时间换空间(dict/skiplist)
8.2 空间换时间思想的应用
场景1:Zset的skiplist + dict
空间开销:
- 跳表节点:每层指针 + score + member
- 字典节点:key + value + 指针
时间收益:
- 按排名查找:O(log n) 跳表
- 按member查找:O(1) 字典
结论:用2倍内存换取高效查询
场景2:quicklist的ziplist节点
默认每个ziplist节点最大64KB
- 节点太小:指针开销大
- 节点太大:内存碎片多
配置参数:list-max-ziplist-size -2
- -2表示单个节点最大8KB
8.3 编码转换的影响
转换是单向的:
Hash: ziplist -> dict
一旦数据量超过阈值,变成dict后
即使数据减少,也不会变回ziplist
Set: intset -> dict
同理,dict不会变回intset
业务建议:
- 数据量稳定的场景:编码转换影响小
- 数据量波动的场景:提前预估容量
九、查看Redis内部结构
9.1 查看对象编码
bash
# 查看单个key的编码类型
OBJECT ENCODING key
# 查看key的内存占用
MEMORY USAGE key
# 查看key的所有信息
DEBUG OBJECT key
9.2 编码类型对照表
| OBJECT ENCODING返回值 | 实际数据结构 |
|---|---|
| int | 整数 |
| embstr | 嵌入式字符串 |
| raw | SDS动态字符串 |
| quicklist | 快速列表 |
| ziplist | 压缩列表 |
| intset | 整数集合 |
| hashtable | 哈希表 |
| skiplist | 跳表 |
9.3 配置参数
bash
# String类型阈值(一般不需调整)
string-max-int-size 512
# Hash类型阈值
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# List类型阈值(每个ziplist节点大小)
list-max-ziplist-size -2
# Set类型阈值
set-max-intset-entries 512
# Zset类型阈值
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
十、面试追问FAQ
| 问题 | 答案 |
|---|---|
| Redis为什么用跳表而不是红黑树? | 跳表实现简单,区间查找更高效,调节参数可控制性能 |
| 为什么要两种编码? | 数据少用空间优化,数据多用性能优化,动态切换 |
| embstr为什么限制44字节? | embstr一次内存分配,44字节刚好能用一次分配完成 |
| 压缩列表的优缺点? | 优点:内存紧凑;缺点:插入删除需要移动元素 |
| ziplist什么时候会连锁更新? | 连续插入/删除长度接近255的节点时可能触发 |
| Redis对象和底层编码的关系? | 一个对象类型可以对应多种编码,根据数据量动态选择 |
根据零声教育教学写作https://github.com/0voice