- Redis 的底层数据结构其实是经常变化的,不光是 Redis 6 到 Redis 7 这样的大版本,就算同样大版本下的不同小版本,底层结构也是经常有变化的。下面讲解使用的 Redis 版本是7.2.5;
1 整体理解 Redis 底层数据结构
1.1 Redis 的底层数据结构——编码
在 Redis 中,为了极致的性能和内存效率,同一种数据类型(如 String, Hash, List, Set, Sorted Set)可以根据其存储内容的特点,采用多种不同的底层数据结构来实现。这个“底层数据结构”就是编码(Encoding);
robj
(Redis Object)结构体中的encoding
字段(下面会讲)就用于记录当前对象使用的是哪一种编码方式;
可以用
OBJECT ENCODING <key>
命令查看某个键值对底层的编码类型。例:127.0.0.1:6379> OBJECT HELP 1) OBJECT <subcommand> [<arg> [value] [opt] ...]. Subcommands are: 2) ENCODING <key> 3) Return the kind of internal representation used in order to store the value 4) associated with a <key>. 5) FREQ <key> 6) Return the access frequency index of the <key>. The returned integer is 7) proportional to the logarithm of the recent access frequency of the key. 8) IDLETIME <key> 9) Return the idle time of the <key>, that is the approximated number of 10) seconds elapsed since the last access to the key. 11) REFCOUNT <key> 12) Return the number of references of the value associated with the specified 13) <key>. 14) HELP 15) Print this help. 127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> OBJECT ENCODING k1 "embstr"
- 这说明
k1-v1
这个 String 类型的键值对,底层用embstr
编码存储;
- 这说明
server.h
(880行)中定义了 Redis 对象的内部编码(encoding)类型:/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* No longer used: old hash encoding. */ #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ #define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */ #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ #define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
编码常量 值 用途说明 OBJ_ENCODING_RAW 0 最原始的动态字符串(SDS)表示,用于较大的字符串。 OBJ_ENCODING_INT 1 将字符串编码为整数。用于可以用 long 类型存储的数值型字符串,节省内存。 OBJ_ENCODING_HT 2 哈希表(Hash Table)。用于存储普通 Set 和 Hash 类型的主体实现。 OBJ_ENCODING_ZIPMAP 3 已废弃。早期用于小哈希表的紧凑编码。 OBJ_ENCODING_LINKEDLIST 4 已废弃。早期用于双向链表实现的 List。 OBJ_ENCODING_ZIPLIST 5 已废弃。早期用于 List, Hash, Sorted Set 的紧凑型编码,后被 Listpack 取代。 OBJ_ENCODING_INTSET 6 整数集合。当 Set 中的元素全是整数且数量较少时,使用这种非常紧凑的编码。 OBJ_ENCODING_SKIPLIST 7 跳跃表。用于 Sorted Set 的实现,支持高效的区间查询。 OBJ_ENCODING_EMBSTR 8 嵌入式字符串。用于存储较短的字符串(<=44字节),将 Redis 对象头与字符串数据分配在同一块连续内存中,提高缓存效率。 OBJ_ENCODING_QUICKLIST 9 快速列表。List 类型的现代实现,是双向链表和 Listpack 的混合体,兼顾了内存效率和操作性能。 OBJ_ENCODING_STREAM 10 流。用于 Stream 数据类型的底层实现,是基于 Radix Tree(基数树)和 Listpack 的复杂结构。 OBJ_ENCODING_LISTPACK 11 列表包。一种更现代化、更紧凑的编码,旨在取代 ziplist,用于 Hash, Sorted Set 和 List(作为 quicklist 的节点)的底层实现。 例:
- 一个 String 值:
- 如果你设置
set mykey 100
,Redis 可能会用OBJ_ENCODING_INT
来存储它; - 如果你设置
set mykey "Hello, a very long string that is more than 44 bytes..."
,Redis 则会使用OBJ_ENCODING_RAW
; - 如果你设置
set mykey "Hello"
(一个短字符串),Redis 则会使用更高效的OBJ_ENCODING_EMBSTR
;
- 如果你设置
- 一个 Hash 值:
- 当 Hash 中的字段和值都很小且数量不多时,Redis 可能会使用
OBJ_ENCODING_LISTPACK
来节省内存; - 当 Hash 变得越来越大时,Redis 会自动将其编码转换为
OBJ_ENCODING_HT
(哈希表),以保证操作效率;
- 当 Hash 中的字段和值都很小且数量不多时,Redis 可能会使用
- 一个 String 值:
在上面的注释中还可以看到。这些编码方式都是使用在 Object 的
encoding
字段里,那么这个 Object 是什么呢?server.h
(900行)struct redisObject { unsigned type:4; // 应用层数据类型(如 String、Hash 等) unsigned encoding:4; // 底层编码类型(如 embstr、ziplist、skiplist 等) unsigned lru:LRU_BITS; // LRU/LFU 淘汰策略相关(内存满时清理冷数据) int refcount; // 对象引用计数(用于内存回收) void *ptr; // 指向真正底层数据结构的指针 };
type
字段:标识 Redis 应用层的数据类型,像string
、hash
、set
等,可通过type key
指令查看;encoding
字段:表示 Redis 底层的编码类型。不同编码对应不同的底层实现,比如string
类型可能有int
、raw
、embstr
等编码,且一些旧的编码(如ZIPLIST
)在 Redis7 中已被listpack
替代;lru
字段:和内存淘汰策略相关。当内存超出配置限制时,Redis 会采用 LRU(最近最少使用)或 LFU(最不经常使用)算法清理“冷数据”(长期不访问的数据),相关配置在redis.conf
中;refcount
字段:记录对象的引用次数(用于内存回收)。当引用次数为 0 时,对象会被释放。可以通过OBJECT REFCOUNT key
指令查看某个键对应对象的引用次数;ptr
字段:是一个指针,指向真正存储数据的底层结构,encoding
只是类型描述,实际数据存储在ptr
所指向的具体结构里。
1.2 应用层类型+底层多结构自适应机制
在 Redis 中,应用层的一种数据类型(如 String),底层会根据数据特征选择不同的存储结构,并非简单的“一对一”对应。例:
127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> type k1 string 127.0.0.1:6379> object encoding k1 "embstr" 127.0.0.1:6379> set k2 1 OK 127.0.0.1:6379> type k2 string 127.0.0.1:6379> object encoding k2 "int" 127.0.0.1:6379> set k3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa OK 127.0.0.1:6379> type k3 string0 127.0.0.1:6379> OBJECT ENCODING k3 "raw"
当存储短字符串
v1
时,用OBJECT ENCODING
查看,底层编码是embstr
;当存储整数
1
时,底层编码是int
;当存储超长字符串时,底层编码是
raw
;说明:String 这种应用层类型,底层会根据值的类型(字符串/整数)、长度,选择最高效的存储结构;
Redis 提供了
DEBUG OBJECT <key>
命令,用于查看键的底层内部结构(如引用计数refcount
、编码encoding
、LRU 空闲时间等)。但该命令默认关闭,需要修改 Redis 配置文件(将enable-debug-command
设为local
),重启服务后才能使用。例:127.0.0.1:6379> DEBUG object k1 Value at:0x7f0e36264c80 refcount:1 encoding:embstr serializedlength:3 lru:7607589 lru_seconds_idle:23
refcount
:对象的引用次数;encoding
:底层编码类型;lru_seconds_idle
:最近一次访问后空闲的秒数(与内存淘汰策略相关);
Redis 会持续优化底层存储,不同版本的同一应用层类型,底层结构可能不同。最核心的变化是 Redis 7 用
listpack
替代了 Redis 6 的ziplist
(为了更高效的存储与操作)。以下是 Redis 6 和 Redis 7 中,常见应用层类型与底层结构的对应关系:Redis 版本 String Set ZSet List Hash Redis 6 SDS(动态字符串) intset + hashtable skiplist + ziplist quicklist + ziplist hashtable + ziplist Redis 7 SDS intset + listpack + hashtable skiplist + listpack quicklist + listpack hashtable + listpack SDS(Simple Dynamic String):简单动态字符串,是 Redis 自定义的动态字符串,用于高效存储 String 类型(支持动态扩容、二进制安全等);
intset:整数集合,当 Set 中所有元素都是整数且数量少时,用 intset 存储(节省空间);元素变多或有非整数时,会转为 hashtable;
skiplist(跳表):ZSet 的核心结构之一,支持高效的范围查询、按分数排序;
ziplist/listpack:紧凑的列表结构,用于 Hash、List、ZSet 等类型(存储少量元素时节省空间)。Redis 7 用
listpack
替代ziplist
,因为listpack
更高效(无需冗余的“回退指针”,内存利用更优);quicklist:由多个 ziplist/listpack 组成的双向链表,平衡了“紧凑存储”和“快速插入/删除”的需求;
hashtable:哈希表,当集合元素较多时,用 hashtable 保证查询效率(时间复杂度 O(1))。
2 String 数据结构详解
2.1 String 底层存储的“自适应”逻辑
Redis 的 String 类型会根据值(value)的类型和长度,选择不同的底层存储方式,以平衡“存储效率”和“操作性能”。具体规则如下:
存储类型 适用场景 int
值可以转换为 long
类型的整数(注意:浮点数会被转为字符串存储)embstr
值是字符串类型,且长度小于 44 字节(底层用 SDS
实现)raw
值是字符串类型,且长度大于等于 44 字节(底层也用 SDS
,但存储形式更“松散”)源码验证如下:
在客户端执行一个
set k1 v1
这样的指令,会进入t_string.c
的setComand
方法:这个
tryObjectEncoding
的方法实现,在object.c
(614行)中,关键部分如下:尝试将字符串转为
long
整数存储(int
类型分支):- 逻辑:如果字符串是“短整数”(长度 ≤20,且能转
long
),且满足“共享条件”(值在 0~999 之间,内存策略允许),则复用共享整数对象(减少内存开销);否则,将对象编码为int
,用ptr
直接存整数值;
- 逻辑:如果字符串是“短整数”(长度 ≤20,且能转
尝试用
embstr
存储短字符串(长度 < 44 字节分支)- 若字符串无法转为
long
,则检查长度是否小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT
(即 44 字节); - 若字符串是“短字符串”(长度 < 44),则创建
embstr
类型对象(底层用SDS
紧凑存储,对象头和SDS
连续分配内存,减少内存碎片);
- 若字符串无法转为
用
raw
存储长字符串(长度 ≥ 44 字节分支)- 若字符串既不是“可转
long
的整数”,也不是“短字符串”,则默认用raw
存储; raw
也是基于SDS
,但对象头和SDS
是分开分配内存的,适合存储长字符串(避免因字符串过长导致“连续内存分配”失败)。
- 若字符串既不是“可转
2.2 int
、embstr
、raw
有什么区别?
2.2.1 int
类型:整数的高效存储
当 String 的值是可转为
long
类型的整数时,Redis 会用int
类型存储。核心特点是:redisObject
的ptr
直接指向整数(而非字符串),减少内存开销;对于 0~999 范围内的整数,Redis 会复用“共享整数对象”(类似 Java 整数池),进一步节省内存;
底层结构中
redisObject
的关键字段内容如下(以set k1 123
为例):type
:REDIS_STRING
(应用层类型是 String);encoding
:OBJ_ENCODING_INT
(底层编码为int
);ptr
:直接指向整数123
(而非字符串“123”)。
2.2.2 embstr
类型:短字符串的紧凑存储
当 String 的值是长度 < 44 字节的字符串时,Redis 会用
embstr
类型存储。核心特点是:底层基于
SDS
(Simple Dynamic String,简单动态字符串)实现,但redisObject
和SDS
的内存是连续分配的(减少内存碎片,提升访问效率);embstr
是“只读”的:若对embstr
执行修改操作,Redis 会将其转为raw
类型后再修改;
底层结构(以
set k1 v1
为例)redisObject
与SDS
连续存储:创建embstr
时,Redis 会一次性分配一块连续内存,同时容纳redisObject
和SDS
(通过createEmbeddedStringObject
方法实现);SDS
的结构:SDS
是 Redis 对字符串的封装,包含len
(已用长度)、alloc
(总分配长度)、flags
(类型标记)、buf
(字符数组)。不同长度的字符串会匹配不同的SDS
结构(如sdshdr8
、sdshdr16
,按需选择,节省内存);
补充:
SDS
是 Redis 为解决 C 字符串缺陷(如无法高效获取长度、非二进制安全等)而设计的封装结构,优势的是:O(1) 获取长度:通过
len
字段直接获取字符串的长度,无需遍历;二进制安全:用
len
标识结束,而非\0
,支持存储任意二进制数据;预分配/惰性释放:优化字符串增长/缩短时的内存操作,减少性能损耗;
不同长度的字符串会自动匹配
sdshdr8
/sdshdr16
等结构,进一步节省内存。
2.2.3 raw
类型:长字符串的通用存储
当 String 的值是长度 ≥ 44 字节的字符串时,Redis 会用
raw
类型存储。核心特点是:底层也基于
SDS
,但redisObject
和SDS
的内存是分开分配的(避免长字符串导致“连续内存分配”失败);支持修改操作,因为内存独立,修改时无需整体搬迁;
底层结构(以
set k1 aaaaaa......
为例)redisObject
的ptr
指向独立分配的SDS
结构;SDS
包含len
、alloc
、flags
、buf
,与embstr
的SDS
结构一致,但内存与redisObject
分离;
2.3 小结
存储类型 | 适用场景 | 底层基于 SDS ? |
内存分配方式 | 可修改性 |
---|---|---|---|---|
int |
可转为 long 的整数 |
否 | ptr 直接指向整数 |
不可修改(整数无需修改) |
embstr |
长度 < 44 字节的字符串 | 是 | 连续分配(对象 + SDS) | 只读(修改会转 raw ) |
raw |
长度 ≥ 44 字节的字符串 | 是 | 分开分配(对象 + SDS) | 可修改 |
3 Hash 类型数据结构详解
3.1 Hash 底层存储的“自适应优化”逻辑
Redis 的 Hash 类型会根据字段数量和字段值大小,自动选择两种底层存储结构之一:
listpack
:适用于字段数量少 + 字段值小的场景;hashtable
:适用于字段数量多 + 字段值大的场景;
Redis 通过两个配置参数,决定何时从
listpack
切换到hashtable
:hash-max-listpack-entries
:Hash 中字段的最大数量(默认 512)。若字段数超过该值,底层会从listpack
转为hashtable
;hash-max-listpack-value
:Hash 中单个字段值的最大字节数(默认 64)。若任意字段值超过该值,底层会从listpack
转为hashtable
;
127.0.0.1:6379> hset user:1 id 1 name roy # 创建哈希对象user:1,包含2个字段(id=1, name=roy)
(integer) 2
127.0.0.1:6379> type user:1
hash
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack" # 查看当前编码方式为listpack(紧凑的线性结构,内存效率高)
# 修改哈希编码的配置参数:
127.0.0.1:6379> config set hash-max-listpack-entries 3 # - hash-max-listpack-entries 3: listpack最多允许3个字段
OK
127.0.0.1:6379> config set hash-max-listpack-value 8 # - hash-max-listpack-value 8: 每个字段值最大8字节
OK
127.0.0.1:6379> hset user:1 name royaaaaaaaaaaaaaaaa # 更新name字段的值(长度超过8字节的"royaaaaaaaaaaaaaaaa")
(integer) 0
127.0.0.1:6379> OBJECT ENCODING user:1 # 由于值大小超过了配置的8字节阈值,触发编码转换,编码已从listpack转换为hashtable
"hashtable" # 字典结构,适合大数据量
127.0.0.1:6379> hset user:2 id 1 name roy score 100 age 18 # 创建新哈希user:2,包含4个字段
(integer) 4
127.0.0.1:6379> OBJECT ENCODING user:2
"hashtable" # 字段数量超过配置的3个阈值,直接使用hashtable编码
两种存储结构的特点与适用场景:
存储结构 适用场景 优势 劣势 listpack
字段少 + 字段值小 紧凑存储(节省内存)、访问快 字段多/值大时,操作效率下降 hashtable
字段多 + 字段值大 大数量/大值场景下,操作效率高 内存开销比 listpack
大补充:Redis 7 用
listpack
替代了 Redis 6 的ziplist
(两者设计思路相似,都是为了“紧凑存储”)。为保证兼容性,Redis 7 仍保留了ziplist
相关的代码和配置,但推荐使用listpack
。
3.2 底层实现
回顾一下 Hash 的结构:
Redis Hash 类型的底层存储(当使用
hashtable
编码时),依赖两个核心结构:dict
结构(代表 Hash 的“值”整体):dict
是 Hash 类型的“容器”,管理所有字段(field)和值(value)的存储与查询(dict.h
84行);struct dict { // 代表一个hash的整体 dictType *type; // 类型特定的操作函数(如哈希、比较) dictEntry **ht_table[2]; // 哈希表数组(双哈希表,用于渐进式 rehash),代表hash中的一个键值对 unsigned long ht_used[2]; // 两个哈希表各自的已用节点数 long rehashidx; // rehash 进度标记(-1 表示未进行 rehash) // 其他字段(如内存对齐、元数据等) };
dictEntry
结构(代表 Hash 中的一个“字段-值”对):每个dictEntry
存储一个field -> value
的映射,通过next
解决哈希冲突(拉链法)(dict.c
63行);struct dictEntry { void *key; // 字段(field) union { void *val; // 值(value)的通用指针 uint64_t u64; // 整数值(若值是整数) int64_t s64; // 长整数值 double d; // 浮点数值 } v; struct dictEntry *next; // 哈希冲突时的链表下一个节点 // 其他字段(如元数据) }
当执行
HSET key field1 value1 field2 value2
时,Redis 底层会经历以下步骤(t_hash.c
606行):入口函数:
hsetCommand
hashTypeTryConversion
函数是 Hash 类型“自动选择存储结构”的核心逻辑,它会根据字段数量和字段值大小,决定是否将listpack
转为hashtable
:- 上方红框:检查“字段数量”是否超限;
- 下方红框:检查“字段值大小”是否超限;
3.3 listpack
结构详解
3.3.1 ziplist
listpack
是ziplist
的升级版,所以谈到listpack
就不得不谈ziplist
;ziplist
是 Redis 为紧凑存储小数据设计的结构,它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用CPU缓存,而且可以针对不同长度的数据进行响应的编码,这种方法可以及有效的节省内存开销,常用于 Hash、List 等类型的底层(小数据场景);ziplist
是由连续内存块组成的顺序性数据结构(类似数组),可以在任意一端进行push
/pop
操作,时间复杂度都是 O(1)。整体结构如下:每个 entry 可以认为是保存的 Hash 的一个键值对,包含三部分:
previous_entry_length
:记录前一个节点的长度(占 1 或 5 字节);- 若前一节点长度 < 254 字节,用 1 字节来保存长度值;
- 否则用 5 字节,首字节是
0xFE
,后 4 字节才存储真实长度;
为什么要这样?因为数字
255
(即0xFF
) 这个单字节值被ziplist
结构保留为特殊的结束标记,不能再用来表示一个普通的“前一个节点长度“;encoding
:编码属性,标记content
的类型和长度;content
:实际存储的数据;
ziplist
的用计算换空间的思想:不再保存指针,只保留长度。极致压缩内存空间:
- 传统链表(True Linked List):每个节点除了存储数据,还包含
prev
和next
两个指针(在64位系统中,每个指针占8字节)。这意味着存储一个很小的数据(比如整数1
)也可能需要额外付出 16 字节的指针开销,内存利用率低; - Ziplist(压缩列表):它完全摒弃了指针。整个
ziplist
是一大块连续的内存。每个entry
只记录前一个entry的字节长度 (previous_entry_length
)。要找到前一个节点,就用当前节点的地址减去这个长度值。通过这种计算方式,省去了巨大的指针开销,实现了内存的“极致压缩”;
- 传统链表(True Linked List):每个节点除了存储数据,还包含
可以通过头部的三个字段直接找到队列的第一个元素和最后一个元素:
一个
ziplist
的结构如下所示,它在头部有几个固定的字段:zlbytes
(4字节)zltail
(4字节)zllen
(2字节)entry1
entry2
… entryN
zlend
(1字节)zlbytes
:记录整个ziplist
占用的总内存字节数。知道了开头,就能直接定位结尾;zltail
:记录最后一个 entry 的起始地址距离ziplist
起始地址有多少字节。直接偏移这个值,就能瞬间找到最后一个元素,所以RPOP
之类的操作也很快;zllen
:记录 entry 的总数。如果小于65535
,这个值就是准确的;否则需要遍历才能知道真实数量;
只能从前往后单向遍历去找到某一个中间的元素,所以
ziplist
不太适合存储太多的元素:- 这是
ziplist
为节省空间所付出的代价,也是它最主要的缺点; - 原因:因为每个 entry 只记录了前一个 entry 的长度,所以无法直接知道后一个 entry 的位置。要找到第 N 个元素,唯一的办法就是从头部(或尾部,如果从尾部开始则需要先找到尾部)开始,一个一个地解析
previous_entry_length
和entry
本身的编码,才能跳转到下一个 entry; - 时间复杂度:访问头尾是 O(1),但访问中间任意位置的元素是 O(N) 的线性时间复杂度;
- 如果元素数量非常多(例如成千上万个),执行一次
LRANGE 1000 1010
这样的命令,就需要从开头遍历超过 1000 个元素,性能会非常差。
- 这是
3.3.2 ziplist
的“连锁更新”问题
然后,为什么要用
listpack
替换ziplist
呢?Redis 的作者 antirez 在 GitHub 上提供了
listpack
的实现,里面有一个md文档介绍了listpack
。文章地址:listpack/listpack.md at master · antirez/listpack · GitHub。listpack
的整体结构和ziplist
差不多,只是做了一些小调整,最核心的原因是要解决ziplist
的连锁更新问题;连锁更新问题的核心就是在 entry 的
previous_entry_length
的记录方式:如果前一个节点的长度小于 254 字节,那么previous_entry_length
的大小为 1 个字节。如果大于等于 254 字节,则previous_entry_length
的大小需要扩展到 5 个字节;例:
假设有这么一个 ziplist,每个 entry 的长度都在 250~253 字节 之间,此时每个 entry 的
previous_entry_length
用 1 字节存储;若在表头插入一个长度 ≥ 254 字节的新 entry:
- 新 entry 的下一个节点(原表头 entry)的
previous_entry_length
需要从 1 字节扩展为 5 字节; - 原表头 entry 长度因此增加,导致它的下一个节点的
previous_entry_length
也需要扩展; - 该过程会连续触发后续所有节点的
previous_entry_length
扩展,形成连锁更新,带来额外的内存操作和性能损耗;
- 新 entry 的下一个节点(原表头 entry)的
3.3.3 listpack
对于连锁更新问题的解决
Redis 7 用
listpack
替代ziplist
,核心改进是让 entry 记录“自身长度”而非“前一节点长度”,从而避免连锁更新;整体结构:与
ziplist
类似,但调整了部分字段total-bytes
:整体占用字节数;num-elements
:entry 数量;entry
:多个列表节点;listpack-end-byte
:表尾标记;
每个 entry 存储“自身长度”,结构如下(
listpack.h
49行):typedef struct { unsigned char *sval; // 字符串值(若为字符串类型) uint32_t slen; // 字符串长度 long long lval; // 整数值(若为整数类型,此时 sval 为 NULL) } listpackEntry;
核心改进:entry 不再依赖“前一节点长度”,而是记录自身长度,因此插入/删除节点时,不会触发后续节点的连续更新,解决了连锁更新问题。
3.4 小结
- Hash 底层更多的是使用
listpack
来存储; - 如果 Hash 对象保存的键值对超过512个,或者所有键值对的字符串长度超过64字节,底层的数据结构就会由
listpack
升级成为hashtable
; - 对于同一个 Hash 数据,
listpack
结构可以升级为hashtable
结构,但是hashtable
结构不会降级成为listpack
。
4 List 类型数据结构详解
4.1 List 底层存储的“自适应选择”逻辑
Redis 的 List 类型会根据元素数量和元素大小,自动选择两种底层存储结构之一:
listpack
:适用于元素数量少 + 元素值小的场景;quicklist
:适用于元素数量多 + 元素值大的场景;
Redis 通过
list-max-listpack-size
参数,决定何时从listpack
切换到quicklist
。该参数的含义是:每个listpack
节点允许存储的最大大小(或元素数量)。其在redis.conf
文件中有更详细的描述:# Lists are also encoded in a special way to save a lot of space. # The number of entries allowed per internal list node can be specified # as a fixed maximum size or a maximum number of elements. # For a fixed maximum size, use -5 through -1, meaning: # -5: max size: 64 Kb <-- not recommended for normal workloads # -4: max size: 32 Kb <-- not recommended # -3: max size: 16 Kb <-- probably not recommended # -2: max size: 8 Kb <-- good # -1: max size: 4 Kb <-- good # Positive numbers mean store up to _exactly_ that number of elements # per list node. # The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size), # but if your use case is unique, adjust the settings as necessary. # -- 每个list中包含的节点大小或个数。正数表示个数,负数-1到-5表示大小。 list-max-listpack-size -2
参数说明:
正数:表示每个
listpack
节点最多包含精确的元素个数。例如:list-max-listpack-size 512
,表示每个节点最多存512个元素;负数:表示每个
listpack
节点最多占用的内存字节数;提供预定义的几个级别(基于2的幂次方):
-5:每个节点最大 64 KiB(65536字节)← 不推荐常规使用
-4:每个节点最大 32 KiB(32768字节)← 不推荐
-3:每个节点最大 16 KiB(16384字节)← 可能不推荐
-2:每个节点最大 8 KiB(8192字节)← 推荐值(性能均衡)
-1:每个节点最大 4 KiB(4096字节)← 推荐值(内存更紧凑)
比如:
list-max-listpack-size -2
,表示每个listpack
节点大小上限为8KB(8192字节),当一个listpack
节点达到这个大小时,新的元素会存入新的listpack
节点;
值越小(如 -1): 内存碎片更少,内存利用率可能更高。但节点数量增多,略微增加内存开销(需要更多指针连接节点);
值越大(如 -4, -5):节点数量更少,指针开销降低。但内存可能产生更多内部碎片,大型节点的插入/删除可能效率略低;
最高性能选项通常是 -2(8Kb)或 -1 (4Kb)。原因:
内存效率:4-8KB 是现代计算机内存管理系统(如内存分页)的一个高效工作区间
缓存友好:适中的节点大小能更好地利用CPU缓存
操作平衡:在此大小下,对节点进行插入、删除等操作仍然保持高效,同时避免了单个节点过大或过小带来的弊端
127.0.0.1:6379> lpush l1 a1 # 向列表l1左侧推入元素"a1",创建列表并包含1个元素
(integer) 1
127.0.0.1:6379> rpush l1 a2 # 向列表l1右侧推入元素"a2"
(integer) 2
127.0.0.1:6379> type l1
list
127.0.0.1:6379> OBJECT ENCODING l1 # 当列表元素数量少且值较小时,Redis默认使用listpack编码以节省内存
"listpack"
# 修改列表编码的配置参数:控制quicklist中每个节点的大小阈值
127.0.0.1:6379> config set list-max-listpack-size 2 # 每个quicklist节点的listpack最多包含2个元素
OK
127.0.0.1:6379> lpush l3 a1 a2 a3 # 创建新列表l3,一次性从左侧推入3个元素"a1","a2","a3"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING l3 # 由于设置了list-max-listpack-size为2,而推入了3个元素,Redis自动选择使用quicklist编码,它将多个listpack节点用双向链表连接起来
"quicklist"
两种存储结构的特点与适用场景
存储结构 适用场景 优势 劣势 listpack
元素少 + 元素值小 紧凑存储(节省内存)、访问快 元素多/值大时,操作效率下降 quicklist
元素多 + 元素值大 大数量/大值场景下,操作效率高 内存开销比 listpack
大Redis 7 及以后版本,List 类型默认优先用
listpack
存储(小数据场景),大数据场景自动切换为quicklist
。
4.2 底层实现
回顾一下 List 的结构:
当执行
LPUSH
或RPUSH
命令时,Redis 底层会经历以下步骤;入口函数:
pushGenericCommand
(t_list.c
484行)createListListpackObject
:创建一个listpack
结构,保存 List 中的元素(object.c
242行)listTypeTryConversionAppend
是 List 类型“自动选择存储结构”的核心逻辑,它会调用listTypeTryConversionRaw
,进一步触发listTypeTryConvertListpack
,判断是否将listpack
转为quicklist
(t_list.c
132行);在这个
listTypeTryConvertListpack
方法中,终于看到了quicklist
:根据元素总字节数和元素总数量,结合配置list_max_listpack_size
(每个 listpack 节点的最大限制),判断是否需要将listpack
转为quicklist
(t_list.c 32
行);在上面这个方法中,涉及到服务端的另一个配置参数
list-compress-depth
,表示 List 的数据压缩级别。可以去配置文件中了解一下:# Lists may also be compressed. # Compress depth is the number of quicklist ziplist nodes from *each* side of # the list to *exclude* from compression. The head and tail of the list # are always uncompressed for fast push/pop operations. Settings are: # 0: disable all list compression # 1: depth 1 means "don't start compressing until after 1 node into the list, # going from either the head or tail" # So: [head]->node->node->...->node->[tail] # [head], [tail] will always be uncompressed; inner nodes will compress. # 2: [head]->[next]->node->node->...->node->[prev]->[tail] # 2 here means: don't compress head or head->next or tail->prev or tail, # but compress all nodes between them. # 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail] # etc. list-compress-depth 0
quicklist
是由多个listpack
(或旧版ziplist
)组成的双向链表,还支持节点压缩(通过list-compress-depth
配置):压缩深度为
0
:不压缩;压缩深度为
1
:只压缩链表中间的节点,保留头尾节点不压缩(兼顾性能和内存);压缩深度为
2
:压缩更内层的节点,以此类推。
4.3 quicklist
结构详解
4.3.1 listpack
的不足
listpack
可视为数组型结构(连续内存存储),优势是:随机访问快(通过偏移量可快速定位元素);
适合
RANGE
等“范围查询”操作;
但劣势也很明显:插入/删除效率低,数组插入/删除元素时,需要挪动后续所有元素的内存位置。当 List 元素较多时,
LPUSH
/RPUSH
等增删操作会因内存搬迁产生较大性能损耗;与数组形成对比的是链表(List)结构。链表的节点之间只通过指针指向相关联的节点,这些节点并不需要占用连续的内存;
- 好处就是对链表节点的增删操作非常方便,只需要调整指针就可以了。所以链表能够非常好的支持 List 数据类型的 LPUSH、LPOP 这样的操作;
- 但是,链表结构也有明显的不足,那就是对数据的检索比较麻烦,只能沿着指针引用关系依次遍历节点;
- 所以纯粹的链表结构也不太适合存储 Redis 的 List 数据类型;
4.3.2 quicklist
的结构组成
为解决
listpack
插入/删除的缺陷,Redis 设计了quicklist
,它是链表 + 数组(listpack
)的混合结构:利用链表的优势:插入/删除元素时,只需修改指针,无需大规模内存搬迁,支持高效的
LPUSH
/RPOP
等操作;利用**
listpack
(数组型)**的优势:每个链表节点内部用listpack
存储元素,保证小范围元素的紧凑存储与快速访问;
quicklist
由quicklist
(整体容器)和quicklistNode
(链表节点)组成,每个quicklistNode
内部封装listpack
存储元素;quicklist
结构(整体容器):管理整个 List 的“链表结构”,记录头尾节点、元素总数等元信息typedef struct quicklist { quicklistNode *head; // 链表头节点 quicklistNode *tail; // 链表尾节点 unsigned long count; // 所有 listpack 中元素的总数量 unsigned long len; // quicklistNode 的数量 // 其他字段(如压缩深度、填充因子等) } quicklist;
quicklistNode
结构(链表节点):作为链表的节点,通过prev
/next
串联成链表;entry
指向内部的listpack
,存储该节点下的具体元素typedef struct quicklistNode { struct quicklistNode *prev; // 前驱节点指针 struct quicklistNode *next; // 后继节点指针 unsigned char *entry; // 指向内部的 listpack(存储具体元素) // 其他字段(如节点大小、元素数量、编码等) } quicklistNode;
quicklist
的整体逻辑链表层:
quicklist
是一个双向链表,由多个quicklistNode
通过prev
/next
指针连接;数组层:每个
quicklistNode
内部的entry
指向一个listpack
,listpack
以数组型连续内存存储该节点下的多个元素;优势融合:
- 链表层保证节点级的插入/删除高效(只需改指针);
- 数组层(
listpack
)保证节点内元素的紧凑存储与快速访问;
Redis 6 及更早版本中,
quicklistNode
内部用ziplist
存储元素;Redis 7 及以后版本,用listpack
替代ziplist
(listpack
是ziplist
的改进版,解决了“连锁更新”等问题)。
4.4 小结
- 如果 List 的底层数据量比较小时,Redis 底层用
listpack
结构保存。当 List 的底层数据量比较大时,Redis 底层用quicklist
结构保存; - 至于这其中数据量大小的判断标准,由参数**
list-max-listpack-size
**决定;- 这个参数设置成正数,就是按照 List 结构的数据节点个数判断;
- 若设置的负数(-1 ~ -5),就是按照数据节点所占的空间大小判断。
5 Set 类型数据结构详解
5.1 Set 底层存储的“多结构自适应”逻辑
Redis 的 Set 类型会根据元素的类型和数量,自动选择三种底层存储结构之一:
intset
:适用于元素全是整数(且在 64 位有符号整数范围内) + 元素数量少的场景;listpack
:适用于元素非整数 + 元素数量少 + 元素值小的场景;hashtable
:适用于元素数量多 + 元素值大的场景;
Redis 通过以下参数,决定不同存储结构的切换阈值:
set-max-intset-entries
(默认 512)作用:当 Set 中整数元素的数量超过该值时,
intset
会自动转为hashtable
;场景:若 Set 元素全是整数(且在 64 位有符号整数范围),且数量 ≤ 512,用
intset
存储;否则转hashtable
;
set-max-listpack-entries
(默认 128)和set-max-listpack-value
(默认 64)set-max-listpack-entries
:Set 中非整数元素的数量阈值;set-max-listpack-value
:Set 中单个非整数元素的最大字节数阈值;场景:若 Set 元素非整数,且“元素数量 ≤ 128”且“所有元素值大小 ≤ 64 字节”,用
listpack
存储;否则转hashtable
;
127.0.0.1:6379> sadd s1 1 2 3 4 5 # 创建集合s1,添加5个整数元素:1, 2, 3, 4, 5
(integer) 5
127.0.0.1:6379> OBJECT ENCODING s1 # 查看编码方式为intset(整数集合)
"intset"
# 当集合中所有元素都是整数且元素数量较少时,Redis使用intset编码
# intset是紧凑的整数数组结构,内存效率极高
127.0.0.1:6379> sadd s2 a b c d e # 创建集合s2,添加5个字符串元素:a, b, c, d, e
(integer) 5
127.0.0.1:6379> OBJECT ENCODING s2 # 查看编码方式为listpack
"listpack"
# 当集合包含非整数元素但元素数量较少时,Redis使用listpack编码
# listpack是紧凑的线性结构,适合存储少量元素
# 修改集合编码的配置参数:
127.0.0.1:6379> config set set-max-listpack-entries 2 # listpack编码最多允许2个元素,超过这个数量,集合将转换为hashtable编码
OK
127.0.0.1:6379> sadd s3 a b c d e # 创建集合s3,添加5个字符串元素:a, b, c, d, e
(integer) 5 # 元素数量(5)超过了配置的阈值(2)
127.0.0.1:6379> OBJECT ENCODING s3 # 编码方式为hashtable(哈希表)
"hashtable"
# 由于元素数量超过了set-max-listpack-entries的限制,Redis自动将编码从listpack转换为hashtable
三种存储结构的特点与适用场景
存储结构 适用场景 优势 劣势 intset
(小整数集合)整数元素 + 数量少 紧凑存储(节省内存)、访问快 仅支持整数,数量多则效率下降 listpack
(小非整数集合)非整数元素 + 数量少 + 值小 紧凑存储、访问快 数量多/值大时,操作效率下降 hashtable
(大集合)数量多 + 值大 / 混合类型元素 大数量/大值场景下,操作效率高 内存开销比 intset
/listpack
大
5.2 底层实现
回顾一下 Set 的结构:
Set 底层核心结构:
intset
,源码定义如下(intset.h
35行):intset
是 Redis 为紧凑存储整数集合设计的结构;- 当 Set 元素全是整数且数量较少时,用
intset
存储,节省内存;
typedef struct intset { uint32_t encoding; // 编码方式(决定整数的存储长度,如 16 位、32 位、64 位) uint32_t length; // 集合中整数的数量 int8_t contents[]; // 存储整数的柔性数组(实际存储的是定长整数,由 encoding 决定长度) } intset;
当执行
SADD key member1 member2 ...
时,Redis 底层会经历以下步骤:入口函数:
saddCommand
(t_set.c
605⾏)存储结构的创建与转换:
setTypeCreate
和setTypeMaybeConvert
。Redis 会根据元素类型和数量,自动选择或转换 Set 的底层存储结构;setTypeCreate
:创建 Set 时选择存储结构(t_set.c
40行)- 小整数集合用
intset
,紧凑省内存; - 小非整数集合用
listpack
,平衡内存与性能;
- 小整数集合用
setTypeMaybeConvert
:已有 Set 时转换存储结构(t_set.c
59行),大集合用hashtable
,保证操作效率;
6 ZSet 类型数据结构详解
6.1 ZSet 底层存储的“双结构混合”逻辑
Redis 的 ZSet 类型会根据元素数量和元素值大小,自动选择两种底层存储结构之一:
listpack
:适用于元素数量少 + 元素值小的场景;skiplist
(跳表):适用于元素数量多 + 元素值大的场景;
Redis 通过两个配置参数,决定何时从
listpack
切换到skiplist
:zset-max-listpack-entries
:ZSet 中元素的最大数量(默认 128)。若元素数超过该值,底层会从listpack
转为skiplist
;zset-max-listpack-value
:ZSet 中单个元素值的最大字节数(默认 64)。若任意元素值超过该值,底层会从listpack
转为skiplist
;
# 查看所有zset(有序集合)相关的配置参数:
127.0.0.1:6379> config get zset*
1) "zset-max-ziplist-value" # 旧版ziplist编码的兼容配置
2) "64"
3) "zset-max-listpack-entries" # listpack编码最多允许128个元素(分值-成员对)
4) "128"
5) "zset-max-ziplist-entries" # 旧版ziplist编码的兼容配置(已逐渐被listpack取代)
6) "128"
7) "zset-max-listpack-value" # 每个成员值最大64字节
8) "64"
127.0.0.1:6379> zadd z1 80 a # 创建有序集合z1,添加一个成员"a"及其分值80
(integer) 1
127.0.0.1:6379> OBJECT ENCODING z1 # 查看编码方式为listpack
"listpack"
# 当元素数量较少(<128)且成员值较小(<64字节)时,Redis使用listpack编码,listpack以紧凑方式存储分值-成员对,内存效率高
# 修改有序集合编码的配置参数:
127.0.0.1:6379> config set zset-max-listpack-entries 3 # listpack编码最多允许3个元素(分值-成员对),超过这个数量,有序集合将转换为skiplist编码
OK
127.0.0.1:6379> zadd z2 80 a 90 b 91 c 95 d # 创建有序集合z2,一次性添加4个成员:a(80), b(90), c(91), d(95)
(integer) 4 # 元素数量(4)超过了新配置的阈值(3)
127.0.0.1:6379> OBJECT ENCODING z2 # 编码方式为skiplist(跳跃表)
"skiplist"
# 由于元素数量超过了zset-max-listpack-entries的限制,Redis自动将编码从listpack转换为skiplist
两种存储结构的特点与适用场景
存储结构 适用场景 优势 劣势 listpack
元素少 + 元素值小 紧凑存储(节省内存)、访问快 元素多/值大时,排序/查询效率下降 skiplist
元素多 + 元素值大 大数量场景下,排序、范围查询效率高 内存开销比 listpack
大skiplist
(跳表)是一种有序数据结构,支持:O(log N) 时间复杂度的按分数查找、范围查找;
高效的插入/删除(平均 O(log N) 时间复杂度);
因此适合 ZSet 这种“需要按分数排序且频繁操作”的场景。
6.2 底层实现
回顾一下 ZSet 的结构:
skiplist
是 Redis 为 ZSet 设计的有序数据结构,用于高效处理按分数排序和范围查询。其核心特点是:多层索引优化:在原始链表之上,构建多层稀疏索引(类似二分查找的思想),加速查找过程;
时间换空间:通过额外的索引层,将查找、范围查询的时间复杂度从 O(N) 优化到 O(log N),但会增加一定内存开销(空间换时间);
读写特性:适合读多写少的场景(写操作需维护索引,有额外开销);
当执行
ZADD key score1 member1 score2 member2 ...
时,Redis 底层会经历以下步骤:入口函数:
zaddCommand
,作为ZADD
命令的入口,调用通用逻辑zaddGenericCommand
(t_zset.c
1838行)
跟踪这个
zaddGenericCommand
方法,可以看到下面这个方法(t_zset.c
1169行):- 情况 1:元素数量少 + 元素值小 → 用 listpack 存储(
if
内的逻辑) - 情况 2:元素数量多 / 元素值大 → 用 skiplist 存储(
if
后的逻辑)
- 情况 1:元素数量少 + 元素值小 → 用 listpack 存储(
6.3 小结
- Redis 底层综合使用
listpack
+skiplist
两种数据结构来保存 ZSet 类型的数据;- 当 ZSet 数据的 value 数据量比较小时,使用
listpack
结构保存; - value 数据量比较大时,使用
skiplist
结构保存。skiplist
是一种典型的用空间换时间的解决方案,适合那些数据量比较大,且读多写少的数据场景。在Redis中使用是非常合适的;
- 当 ZSet 数据的 value 数据量比较小时,使用
- Redis 中衡量 ZSet 的 value 数据大小的参数有两个:
zset-max-listpack-entries
和zset-max-listpack-value
,分别从 value 的元数数量和数据大小两方面进行区分。
7 总结
数据类型 | Redis 6 底层结构 | Redis 7 底层结构 | 优化逻辑 |
---|---|---|---|
string | SDS(动态字符串) | SDS | 无核心变化,SDS 本身已足够高效 |
set | intset + hashtable | intset + listpack + hashtable | 用 listpack 替代 ziplist (解决 ziplist “连锁更新”问题,更稳定高效) |
zset | skiplist + ziplist | skiplist + listpack | 用 listpack 替代 ziplist (同 set 的优化逻辑) |
list | quicklist + ziplist | quicklist + listpack | 用 listpack 替代 ziplist (同 set 的优化逻辑) |
hash | hashtable + ziplist | hashtable + list | 用更高效的结构替代 ziplist ,提升大数量场景下的性能 |
一道经典面试题:Redis 性能优异是多维度优化的结果,核心原因包括:
纯内存操作:所有数据存在内存中,避免磁盘 I/O 开销(最核心原因);
单线程模型:避免多线程上下文切换和锁竞争,简化设计且执行高效;
高效数据结构:如 SDS、
quicklist
、skiplist
等,为不同场景定制,保证操作效率;I/O 多路复用:通过
epoll
/kqueue
等机制,单线程处理多客户端连接,提升网络 I/O 效率;渐进式重哈希:哈希表扩容时,分批次迁移数据,避免单次大开销;
零拷贝等优化:如网络传输时利用操作系统“零拷贝”减少数据拷贝次数;
Redis 不仅是“快”,更在多场景下提供核心能力,成为分布式系统的关键组件:
集中式缓存:缓解数据库压力,提升读性能;
分布式锁:基于
SETNX
等命令实现分布式场景下的锁机制;分布式主键生成:利用
INCR
生成全局唯一 ID;NoSQL 数据库:支持复杂数据结构(Hash、List、Set、ZSet),满足非关系型数据存储需求;
向量搜索:Redis earch 等模块支持向量相似性查询,赋能 AI 场景;
发布订阅:基于
PUBLISH/SUBSCRIBE
实现消息通信。