0. 前言
redis高效存储的本质,是来源它高效的性能设计。这其中,基础的键值对组织方式,是所有设计的基石。首先让我们从基础键值对的组织结构,来了解redis的基础数据类型设计。
1. redis的基础键值对组织
所有的键值对,无论是String、List、Hash、Set、ZSet,都会用一个抽象的通用结构dictEntry放在一个全局hash表里,每个桶单独维护。并通过哈希的方式,去查找每个key。
scss
type dictEntry struct{
key 指针8B (redisObejct16B)
value 指针8B(redisObejct16B)
next 8B
//最后整个结构会按jemalloc进行一次内存对齐
}

1.1 哈希冲突和rehash
哈希冲突一般通过链式哈希解决,但链太长的时候,查找效率还是很低。这时候会进行rehash,把原本的链分解,扩充hash表的大小,解决长链的问题。
1.1.1 rehash的步骤
- redis一开始会有两个hash表,一样的大小,称为A表和B表
- B表会扩容到原本的
两倍大小
- copy A->B
- 释放A表
1.1.2 单线程引入的问题------同时使用和扩容
如果整个线程罢工去做rehash,redis单线程会阻塞。为了解决,redis使用了渐进式hash,每处理一个请求时,顺带把这次的rehash做了。
顺这个思路,如果总有些低频的key访问不到,怎么解决?
这时候会有定时任务,定期扫描hash桶,清理这些key
1.1.3 思维导图

2. redis每种类型是怎么设计的?都有哪些底层数据结构?
经过全局hash桶的查询,这时我们定位到了某个具体的key里,接下来让我们看下,每种类型是怎么设计的
每个 Redis 中存储的值,无论哪种类型(字符串、列表、集合等),都有一个对应的 RedisObject
,用于描述该值的类型和存储方式。
go
// RedisType 表示 Redis 对象的类型。
type RedisType int
const (
StringType RedisType = iota // 字符串类型
ListType // 列表类型
SetType // 集合类型
ZSetType // 有序集合类型
HashType // 哈希类型
// 根据需要添加其他类型
)
// EncodingType 表示 Redis 对象的编码类型。
type EncodingType int
const (
Raw EncodingType = iota // 原始字符串编码
Int // 整数编码
EmbStr // 短字符串优化编码
// 根据需要添加其他编码类型
)
// RedisObject 在 Go 中表示一个 Redis 对象。
type RedisObject struct {
Type RedisType // 对象的类型
Encoding EncodingType // 对象的编码类型
Ptr interface{} // 通用指针,指向实际数据
LRU uint64 // 用于 LRU 淘汰策略的最后访问时间
RefCount int // 内存管理的引用计数
}
可选的类型有:整数数组、双向链表、哈希表、压缩列表和跳表

2.1 简单动态字符串
简单动态字符串,由两个对象组成:
- SDS
- RedisObject
2.1.1 SDS内存布局设计------三种编码方式(int、ebmstr、raw)
int编码
保存整型时,RedisObject中,指向实际数据的指针Ptr就直接赋值为整数数据,节约空间
保存字符串时,RedisObject 会有两种编码格式,ebmstr编码和 raw编码
ebmstr编码:
字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。
raw编码:
当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。
scss
type SDS struct{
buf//字节数组 保存实际数据
Len(4B) //buf 已经使用的长度
alloc(4B) //buf的容量,相当于golang的cap
}
go
type RedisObject struct{
元数据 //(8B)
ptr //(8B) -> SDS
}

2.1.2 实例分析
一个 10 位数的图片 ID 和图片存储对象 ID 都是整数,总共会占用64B。
64B都由哪些部分组成呢?
一共三个部分组成:
-
- key 16B(整数->int编码->16B)
-
- value 16B(整数->int编码->16B)
-
- 全局哈希表dictEntry结构 32B (实际24B,内存对齐后32B)
key、value都是int,所以用int编码,一共16 * 2(key + value) = 32B, 剩下32B,来自全局哈希表。全局hash表结构dicEntry是24B,但是redis的内存对齐,会让他变成2的x次方,所以是32B。
2.2 整数数组
最基础的数组结构,优点是查找快,缺点是插入是O(n)的,不适合经常改动的结构
2.3 压缩列表
压缩列表(ziplist)是一种为节省内存而设计的连续内存数据结构,常用于存储少量数据(如哈希对象、列表对象的早期形态)。它将多个元素紧密排列在一块连续内存中,通过特殊编码方式存储整数和短字符串,从而减少内存碎片。
Golang
type ZipList struct{
Arr []T
Zlbytes int // 整个列表的长度
Zltail int // 到列表尾部的偏移量
Zllen int // 列表中元素的个数
Zlend // 在列表尾部,表示元素结束
}
2.3.1 压缩列表的整体结构
压缩列表的内存布局如下:
xml
<zlbytes> <zltail> <zllen> <entry1> <entry2> ... <entryN> <zlend>
各部分含义:
- zlbytes (4 字节):
整个压缩列表占用的总字节数(包括自身),用于快速定位压缩列表的末尾。 - zltail (4 字节):
压缩列表尾部元素的偏移量(相对于压缩列表起始地址),用于支持从尾部快速访问元素。 - zllen (2 字节):
压缩列表包含的元素数量。若元素数量超过 65535,则该值为 65535,需遍历整个列表获取真实数量。 - entry1 ~ entryN :
压缩列表存储的元素,每个元素包含前一个元素长度 、自身编码 和实际数据。 - zlend (1 字节):
压缩列表的结束标记,固定值为0xFF
(十进制 255)。
2.3.2 压缩列表节点(entry)的结构
每个元素(entry)由三部分组成:
xml
<prevlen> <encoding> <data>
-
prevlen :
前一个元素的长度,用于从后向前遍历列表。
- 若前一个元素长度小于 254 字节,
prevlen
用 1 字节表示; - 若前一个元素长度大于等于 254 字节,
prevlen
用 5 字节表示(第 1 字节固定为0xFE
,后 4 字节存储实际长度)。
- 若前一个元素长度小于 254 字节,
-
encoding :
元素数据的编码方式,决定了
data
字段的类型和长度。常见编码类型:- 字符串编码 :以
00
、01
或10
开头,后跟字符串长度。
例如:00xxxxxx
表示长度 ≤ 63 字节的字符串。 - 整数编码 :以
11
开头,直接存储整数值。
例如:11000000
表示 4 位有符号整数。
- 字符串编码 :以
-
data :
实际存储的数据,格式和长度由
encoding
决定。
2.3.3 整数编码的优化
压缩列表对不同范围的整数采用不同长度的编码:
- 1 字节:存储范围为 -128 ~ 127 的整数。
- 2 字节:存储 16 位有符号整数。
- 5 字节:存储 64 位有符号整数。
例如,整数 5
可能被编码为 11000001
(1 字节表示),而非以字符串形式存储(如 0x35
),从而节省空间。
2.3.4 连锁更新问题
压缩列表的设计存在一个潜在问题:连锁更新 (Cascade Update)。
当插入或删除一个元素时,可能导致后续元素的 prevlen
字段需要扩展(从 1 字节变为 5 字节),这种扩展可能会传播到后续多个元素,引发连锁更新,影响性能。
Redis 通过限制压缩列表的最大长度和元素大小来缓解问题(如 hash-max-ziplist-entries
配置),如果超过限制,就变成跳表或者哈希表。
2.3.5 应用场景
压缩列表在 Redis 中主要用于:
-
哈希对象:当哈希键值对数量较少且值为短字符串或小整数时。
-
列表对象:当列表元素较少且每个元素长度较短时。
-
有序集合:当成员数量较少且分数为小整数时。
当元素数量或大小超过阈值时,Redis 会将压缩列表转换为更适合大数据量的结构(如哈希表、跳表)。
2.3.6 示例:压缩列表的内存布局
假设一个压缩列表包含三个元素:"a"
(字符串)、2
(整数)、"bcd"
(字符串),其内存布局可能如下:
css
[zlbytes=21] [zltail=16] [zllen=3]
[prevlen=0] [encoding="00000001"] [data="a"]
[prevlen=3] [encoding="11000010"] [data=2]
[prevlen=4] [encoding="00000011"] [data="bcd"]
[zlend=0xFF]
2.3.7 总结
压缩列表通过连续内存布局 和特殊编码 ,在小数据场景下显著节省内存。其核心优势是内存紧凑,但缺点是插入和删除操作可能引发连锁更新。Redis 通过合理的转换阈值(如 hash-max-ziplist-entries
)平衡内存效率和操作性能,使其成为轻量级数据结构的首选。
2.4 跳表
在链表的基础上升级的,维护了多级索引,通过索引位置的几个跳转,实现数据的快速定位。
核心的几个元素:
- 基础数据链表结构(数据结点)
- 多级索引(索引结点怎么分层、索引-索引结点、索引-数据结点的关联)
2.4.1 增加层级的时机
在插入新节点时会增加层级。具体来说,当为新节点随机生成的层级大于当前跳表的最大层级时,就会增加跳表的层级。
go
const (
maxLevel = 16
probability = 0.5
)
// randomLevel 随机生成一个层级
func randomLevel() int {
level := 1
// 不断生成随机数进行判断,直到不满足条件或达到最大层级
for rand.Float64() < probability && level < maxLevel {
level++
}
return level
}
新插入一个值为15的结点,会把x层结点链表,更新到15这个新节点上. 以实现层级扩缩容

2.5 hash表
hash表的基本结构就不多介绍了,核心元素:
- key-value
- 哈希冲突
- rehash问题------渐进式hash(类似上一篇)
3. 总结:Redis 高效存储的核心设计原则
3.1 数据结构自适应切换
- 小数据用压缩列表、整数编码等轻量级结构,节省内存;大数据自动转为哈希表、跳表,平衡性能。
3.2 单线程与渐进式优化
- 单线程避免锁竞争,渐进式 Rehash / 迁移无阻塞;定时扫描低频数据保障稳定。
3.3 内存布局优化
embstr
编码、内存对齐减少碎片;引用计数与 LRU 优化内存管理。
3.4 核心数据结构适用场景
结构 | 优势 | 场景 |
---|---|---|
压缩列表 | 内存紧凑 | 小哈希 / 列表 / 有序集合 |
跳表 | 有序数据快查 | 排行榜、范围查询 |
哈希表 | 随机读写高效 | 高频键值对访问 |