Redis 字符串实现详解:SDS 与编码机制(基于 6.0.10 版本)
Redis 中的字符串并非直接使用 C 语言的原生字符串(以 \0 结尾的字符数组),而是通过SDS(Simple Dynamic String,简单动态字符串) 实现。SDS 解决了 C 字符串的诸多缺陷(如长度获取低效、缓冲区溢出风险等),并结合灵活的编码机制,在性能和内存效率上进行了极致优化。本文结合源码结构,详解 SDS 的设计与字符串对象的编码策略。
SDS 的设计动机:为何不使用 C 字符串?
C 语言原生字符串(char*)存在以下缺陷,无法满足 Redis 高性能、高可靠性的需求:
- 长度获取低效:需遍历整个字符串(
strlen函数,O (N) 时间复杂度)。 - 缓冲区溢出风险:修改字符串时(如
strcat),若未提前分配足够空间,会覆盖相邻内存。 - 不支持二进制安全:以
\0作为结束标志,无法存储包含\0的二进制数据(如图片、音频)。 - 修改效率低:每次修改都需重新分配内存(扩展或收缩),频繁操作会导致性能损耗。
SDS 针对这些问题进行了优化,核心目标是:高效获取长度、杜绝缓冲区溢出、支持二进制安全、减少内存重分配次数。
SDS 的结构体设计
SDS 提供了多种结构体(sdshdr5 到 sdshdr64),根据字符串长度选择合适的类型,避免内存浪费。源码结构如下(简化自 sds.h):
1. 核心结构体(以 sdshdr8 为例)
1 | struct __attribute__ ((__packed__)) sdshdr8 { |
字段详解:
len:记录字符串的实际长度(字节数),获取长度时直接返回len,时间复杂度 O (1)(解决 C 字符串的低效问题)。alloc:记录总分配的空间(不包含头部和结尾\0),alloc - len即为未使用的预留空间,用于减少修改时的内存重分配(见下文 “预分配策略”)。flags:低 3 位表示 SDS 类型(SDS_TYPE_5到SDS_TYPE_64),用于区分不同结构体(根据字符串长度自动选择)。buf:存储字符串的字符数组,结尾包含\0(兼容 C 语言字符串函数,如printf),但内容由len控制(支持二进制安全,即使包含\0也能正确处理)。
2. 不同 SDS 类型的区别
Redis 定义了 5 种 SDS 结构体,根据字符串长度选择对应的类型,最小化内存占用:
| 结构体 | len 类型 |
最大长度(字节) | 适用场景 |
|---|---|---|---|
sdshdr5 |
5 位(嵌在 flags 中) |
31 | 极少使用(Redis 源码注释提到暂不启用) |
sdshdr8 |
uint8_t |
255 | 短字符串(≤255 字节) |
sdshdr16 |
uint16_t |
65535 | 中长字符串(≤65535 字节) |
sdshdr32 |
uint32_t |
4294967295 | 长字符串(≤4GB) |
sdshdr64 |
uint64_t |
极大(理论上无上限) | 超长字符串 |
自动类型转换:当字符串长度超过当前结构体的 len 上限时,SDS 会自动升级为更大的类型(如 sdshdr8 升级为 sdshdr16),确保能容纳新长度。
SDS 的核心特性与优势
1. 高效获取长度
通过 len 字段直接获取长度,无需遍历,时间复杂度 O (1)。例如:
1 | // 获取SDS长度(Redis源码函数) |
2. 杜绝缓冲区溢出
修改字符串前,SDS 会先检查 alloc - len 是否足够容纳新内容:
- 若足够,直接修改
buf并更新len。 - 若不足,自动扩展内存(调用
sdsMakeRoomFor函数),再执行修改。
彻底避免了 C 字符串中因空间不足导致的缓冲区溢出。
3. 二进制安全
SDS 以 len 而非 \0 标记字符串结束,因此可以存储任意二进制数据(如包含 \0 的图片数据)。例如:
- 存储字符串 “a\0b”(含
\0)时,len=3,buf实际为{'a', '\0', 'b', '\0'},sdslen返回 3,正确识别完整长度。
4. 减少内存重分配
SDS 通过预分配空间和惰性释放优化内存操作:
- 预分配空间:扩展字符串时,若新长度
new_len≤ 1MB,则分配new_len * 2的空间(预留一倍);若 > 1MB,则额外分配 1MB 空间。减少后续修改的重分配次数。 - 惰性释放:缩短字符串时,不立即回收多余空间,而是更新
len,预留空间供后续扩展使用(可通过sdsRemoveFreeSpace手动释放)。
Redis 字符串对象的编码机制
Redis 中的字符串是 “对象(robj)”,不仅包含 SDS 数据,还包含编码信息(encoding)。根据字符串内容,自动选择三种编码方式,平衡性能与内存:
| 编码方式 | 适用场景 | 存储结构 |
|---|---|---|
OBJ_ENCODING_INT |
字符串可表示为 64 位整数(如 “123”) | 直接存储整数(robj->ptr 指向整数),无 SDS 结构 |
OBJ_ENCODING_EMBSTR |
短字符串(长度 ≤ 44 字节,非整数) | robj 与 sdshdr8 连续存储(一次内存分配) |
OBJ_ENCODING_RAW |
长字符串(长度 > 44 字节,非整数) | robj 与 sdshdr 分开存储(两次内存分配) |
1. OBJ_ENCODING_INT(整数编码)
- 触发条件:字符串是纯整数(如 “123456”),且可被 64 位整数表示。
- 优势:直接以整数形式存储(
robj->ptr指向整数),无需 SDS 结构,节省内存(robj本身仅占 16 字节)。 - 转换:当整数被修改为非整数(如 “123abc”),会转为
EMBSTR或RAW编码。
2. OBJ_ENCODING_EMBSTR(嵌入式编码)
触发条件:非整数字符串,长度 ≤ 44 字节(Redis 6.0 中,
EMBSTR_MAX_SIZE为 44)。存储特点:通过一次内存分配,分配连续空间,依次存储robj和sdshdr8结构(包含buf)。例如:
1
[robj结构][sdshdr8.len][sdshdr8.alloc][sdshdr8.flags][buf...]
优势:内存连续,缓存命中率高(CPU 缓存可一次性加载
robj和 SDS 数据),创建和释放效率高(一次分配 / 释放)。限制:
EMBSTR编码的字符串是 “只读” 的,修改时会先转为RAW编码(因为修改可能需要扩展空间,破坏连续性)。
3. OBJ_ENCODING_RAW(原始编码)
- 触发条件:字符串长度 > 44 字节,或
EMBSTR编码的字符串被修改(如拼接操作)。 - 存储特点:
robj和sdshdr分开分配内存(两次分配),两者在内存中不连续。 - 优势:支持任意长度的字符串,修改时无需担心连续内存的限制。
编码转换示例:
1 | # 1. 整数字符串 → INT 编码 |
EMBSTR 与 RAW 的核心区别
| 特性 | EMBSTR 编码 |
RAW 编码 |
|---|---|---|
| 内存分配次数 | 1 次(连续空间包含 robj 和 sdshdr) |
2 次(robj 和 sdshdr 分开分配) |
| 内存连续性 | 连续(缓存友好) | 不连续(可能降低缓存命中率) |
| 可修改性 | 只读(修改即转为 RAW) |
可直接修改 |
| 适用场景 | 短字符串、只读或极少修改的字符串 | 长字符串、需频繁修改的字符串 |