struct redisobject {
unsigned type:4; // [0-3 bit] 对象类型 (如 string)
unsigned encoding:4; // [4-7 bit] 编码方式 (如 int/embstr/raw)
unsigned lru:24; // [8-31 bit] 缓存淘汰数据
int refcount; // [32-63 bit] 引用计数 (4字节)
void *ptr; // [64-127 bit] 关键指针 (8字节)
};string
在 redis 的底层实现中,string(字符串) 类型并不只有一种形态。为了平衡“内存占用”与“处理性能”,redis 会根据字符串的内容和长度,在 int、embstr 和 raw 三种编码方式之间自动切换。
这三种编码都封装在 redisobject 这个“外壳”下,通过 encoding 字段进行区分。
struct sdshdr8 {
uint8_t len; /* 已使用长度 */
uint8_t alloc; /* 总分配空间(不含头和 \0) */
unsigned char flags; /* 类型标志(如 sdshdr8, sdshdr16 等) */
char buf[]; /* 实际字节数组 */
};1.int编码:直接存储整数
当一个字符串对象保存的是整数值,且这个整数可以用 long 类型(8 字节有符号整数)表示时,redis 就会使用 int 编码。
- 物理特征:它不会分配额外的 sds 空间,而是直接将整数值存储在
redisobject结构体的ptr指针字段中(通过强制类型转换)。 - 共享对象优化:redis 启动时会预先创建 0 ∼ 9999 0 \sim 9999 0∼9999 这 10,000 个整数对象。如果你存的值在这个范围内,所有的 key 都会指向同一个物理内存地址,引用计数加 1,内存开销几乎为零。
- 适用场景:计数器、id 存储等数值场景。
2.embstr编码:嵌入式短字符串
当字符串的长度 小于等于 44 字节 时,redis 使用 embstr 编码。这是为了极致压榨小对象的性能。
- 物理特征:
redisobject和sdshdr(sds 头部及数据)在内存中是连续的一整块。它是通过一次malloc申请出来的。 - 核心逻辑:
- 只读性:它是只读的,任何修改操作(如
append)都会迫使它先升级为raw。 - 高性能:由于内存连续,cpu 缓存命中率极高,且分配/释放内存只需要一次系统调用。
- 只读性:它是只读的,任何修改操作(如
- 计算门槛:44 字节的限制是为了让整个对象(16b
redisobject+ 3bsdshdr8+ 1b\0+ 44b data)刚好适配内存分配器的 64 字节 内存槽位。
3.raw编码:常规长字符串
当字符串的长度 大于 44 字节,或者对 embstr 进行了修改操作时,redis 会使用 raw 编码。
- 物理特征:
redisobject和sdshdr分布在两块不连续的内存空间中。ptr指针指向独立的 sds 区域。 - 核心逻辑:
- 可扩展性:适合存储长文本、二进制数据或频繁修改的字符串。
- 分配代价:创建或销毁对象需要两次
malloc或free。
- 适用场景:json 数据、序列化后的对象、较大的文本内容。
list
redis3.2之前:ziplist/linkedlist
在 redis 3.2 之前,list 的实现非常简单粗暴:当数据量小时使用 ziplist(压缩列表),通过连续内存压榨空间;当数据量大或字符串长时,直接转换为 linkedlist(双向链表),通过指针实现灵活增删,但代价是每个节点都要背负两个 8 字节指针的沉重负担,且内存碎片极多。
redis3.2之后:quicklist
redisobject中的*ptr指向quicklist对象
typedef struct quicklist {
quicklistnode *head; /* 指向头节点 */
quicklistnode *tail; /* 指向尾节点 */
unsigned long count; /* 所有元素总数 */
unsigned long len; /* 节点(车厢)总数 */
int fill : 16; /* 节点填充因子 */
unsigned int compress : 16; /* 压缩深度 */
} quicklist;
typedef struct quicklistnode {
struct quicklistnode *prev; /* 前驱指针 */
struct quicklistnode *next; /* 后继指针 */
unsigned char *zl; /* 指向物理内存中的连续块 (ziplist/listpack) */
unsigned int sz; /* 连续块占用的总字节数 */
unsigned int count : 16; /* 连续块包含的元素个数 */
// ... 其他标志位
} quicklistnode;set
redis 的 set(集合) 编码设计同样遵循“从小到大”的进化逻辑。它在物理实现上主要在 intset(整数集合)、listpack(紧凑列表,redis 7.2+) 和 hashtable(哈希表) 之间切换。
它的核心哲学是:如果全是小整数,我用数组排好序;如果有字符串,我用哈希表锁死。
1. 物理结构:intset(整数集合)
当集合满足以下 两个条件 时,redis 优先使用 intset:
- 集合内所有成员均为 整数。
- 成员数量小于配置参数
set-max-intset-entries(默认 512 个)。
内存布局与查找逻辑
intset 是一块绝对连续的内存空间。
- 物理存储:内部是一个有序数组,支持
int16_t、int32_t或int64_t编码。 - 有序性:元素在数组内按从小到大严格排序。
- 查找算法:使用 二分查找(binary search),时间复杂度为 o ( log n ) o(\log n) o(logn)。
- 升级逻辑:当新插入的整数超出当前位宽(如
int16存入int32)时,会触发整块内存的重新分配和数据迁移。注意,为了保持效率,该过程不可逆(不支持降级)。
2. 物理结构:listpack(紧凑列表)
这是 redis 7.2 引入的新物理层。在旧版本中,集合只要出现一个字符串就会立刻膨胀为 dict,而 listpack 充当了中间的缓冲带。
- 触发场景:集合中包含字符串,但成员数量和单个字符串长度未达到
set-max-listpack-entries和set-max-listpack-value阈值。 - 物理特征:连续字节流存储。
- 性能权衡:虽然查找复杂度退化为 o ( n ) o(n) o(n)(顺序遍历),但由于数据规模极小,其内存利用率远高于
dict,且在小数据量下,连续内存对 cpu 缓存的友好性抵消了 o ( n ) o(n) o(n) 的算法劣势。
3. 物理结构:dict(字典 / 逻辑名称 hashtable)
当集合规模超过阈值,或包含长字符串时,redis 会使用 dict 作为终极物理载体。
物理映射与内存布局
此时 redisobject->ptr 指向一个真实的 dict 结构体实例。
- key (键):存储集合的成员,指向一个 sds 字符串对象。
- value (值):物理上统一设置为
null指针。 - 唯一性保证:直接利用
dict自身的哈希碰撞处理和 key 唯一性逻辑实现集合去重。 - 性能特征:查找复杂度为 o ( 1 ) o(1) o(1)。支持渐进式 rehash,在数据量极大时仍能保持稳定的响应速度。
4. 宏观物理映射:redisobject 的指向
对于 set 来说,redisobject 的包装方式非常直观:
| 字段 | intset 编码 | hashtable 编码 |
|---|---|---|
| type | obj_set | obj_set |
| encoding | obj_encoding_intset | obj_encoding_ht |
| ptr 指向 | 一整块连续的 intset 结构 | 一个复杂的 dict 字典结构 |
zset
redis 的 zset(有序集合) 在底层编码上设计得最为复杂,因为它必须同时满足 o ( 1 ) o(1) o(1) 成员查分 和 o ( log n ) o(\log n) o(logn) 按分数排序/范围检索 这两个核心需求。
其物理实现主要分为两个阶段:listpack 和 dict + zskiplist。
1. 紧凑阶段:listpack(紧凑列表)
当 zset 满足以下两个条件时,redis 使用 listpack 编码(obj_encoding_listpack):
- 成员数量小于
zset-max-listpack-entries(默认 128)。 - 所有成员字符串长度小于
zset-max-listpack-value(默认 64 字节)。
物理存储逻辑
在 listpack 内部,成员(member)和分值(score)被存储为两个相邻的 entry:
- 布局:
[member1, score1, member2, score2, ...] - 有序性:内部元素按分值(score)从小到大严格排序。
- 性能特征:由于是连续内存,插入和查找涉及 o ( n ) o(n) o(n) 的顺序遍历及内存搬迁。但在小数据量下,这种结构的 cpu 缓存命中率极高,且省去了复杂的指针开销。
2. 进化阶段:zset结构体 (跳表 + 字典)
当数据量突破阈值后,redisobject->ptr 会指向一个专门的 zset 结构体。这是一个双重物理结构的组合:
typedef struct zset {
dict *dict; /* 成员 -> 分值的哈希表 */
zskiplist *zsl; /* 按分数排序的跳跃表 */
} zset;a. 物理组件一:dict(字典)
- 作用:实现 o ( 1 ) o(1) o(1) 复杂度的
zscore操作。 - 逻辑:key 是成员(sds),value 是分值(double)。
- 必要性:如果没有
dict,查找一个成员的分数需要遍历跳表,复杂度为 o ( log n ) o(\log n) o(logn)。
b. 物理组件二:zskiplist(跳跃表)
- 作用:实现高效的范围查询(
zrange)和排名计算(zrank)。 - 逻辑:节点按分数排序。每个节点包含多个层级的指针,支持快速跳跃寻址。
- 性能:平均查找复杂度为 o ( log n ) o(\log n) o(logn)。
3. 内存优化:sds 的“引用共享”
你可能会担心:同一个成员既存在 dict 里,又存在 zskiplist 里,岂不是浪费了一倍内存?
物理真相:dict 的 key 和 zskiplistnode 的 ele 指向的是同一个物理内存地址(同一个 sds 对象)。
- redis 只是在两个数据结构中各存了一个 指针。
- 这种设计通过增加少量指针开销(每个节点约几十字节),换取了两个维度的极致查询速度。
4. 物理特性对比表
| 物理结构 | 逻辑编码 (encoding) | 核心优势 | 算法复杂度 | 内存特征 |
|---|---|---|---|---|
| listpack | listpack | 极致节省内存 | o ( n ) o(n) o(n) (查找/插入) | 连续内存,无碎片 |
| zset (复合) | skiplist | 全能性能 | 查分 o ( 1 ) o(1) o(1),范围 o ( log n ) o(\log n) o(logn) | 双重索引,指针较多 |
5. 状态转换逻辑
zset 的转换通常是单向不可逆的:
- 一旦数据量超过阈值,
listpack会被拆解,重新装载进一个新的dict和zskiplist中。 - 原因:从复杂的双重结构回退到连续内存块涉及大规模的内存重分配和 cpu 计算,收益不抵成本。
hash
redis 的 hash(哈希) 结构在底层编码的设计上,逻辑与 zset 非常相似:在数据量小时采用紧凑的连续内存,在数据量大时进化为散列表。
目前的物理实现主要分为 listpack 和 dict 两种。
1. 紧凑编码:listpack(紧凑列表)
当 hash 结构满足以下两个条件时,redis 使用 listpack 存储(编码名称为 obj_encoding_listpack):
- 哈希中字段(field)的数量小于
hash-max-listpack-entries(默认 512 个)。 - 所有字段名和值的长度都小于
hash-max-listpack-value(默认 64 字节)。
物理存储逻辑
在 listpack 的字节流中,field 和 value 是作为两个相邻的 entry 存储的:
- 布局:
[field1, value1, field2, value2, ...] - 查找方式:完全依靠顺序遍历。由于内存是绝对连续的,cpu 在读取时可以利用预取机制(prefetching),在小规模数据下速度极快。
- 内存优势:没有指针开销,没有内存对齐的空隙,空间利用率达到极致。
2. 散列编码:dict(字典)
一旦数据量突破阈值,或者某个 value 太长,redis 就会将物理结构转换为 dict(编码名称为 obj_encoding_ht)。
物理实现逻辑
此时 redisobject->ptr 指向一个真实的 dict 结构体:
- key (键):存储的是 hash 的字段名(field),物理上是一个 sds 对象。
- value (值):存储的是 hash 的字段值(value),物理上同样是一个 sds 对象。
- 冲突处理:使用拉链法(链地址法)解决哈希冲突。
- 性能特征:查找、插入和删除的复杂度均为 o ( 1 ) o(1) o(1)。
到此这篇关于redis数据编码详解的文章就介绍到这了,更多相关redis数据编码内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论