02.1 - String 底层——SDS
定位: Redis 中最基础、最常用的数据类型,底层由 SDS(简单动态字符串)实现
面试高频度: ⭐⭐⭐⭐⭐
考查方式: SDS 结构设计、对比 C 字符串、编码类型(int/embstr/raw)
一、这是什么?为什么需要它?
是什么
SDS(Simple Dynamic String)是 Redis 自定义的字符串抽象类型,它包装了 C 语言字符串,提供了安全、高效的动态字符串操作。
为什么不用 C 字符串?
C 字符串(char*)的四宗罪:
1. O(n) 取长度 —— 没有长度字段,每次 strlen() 遍历
100MB 字符串求长度 → 遍历 100MB 内存
2. 二进制不安全 —— 以 '\0' 结尾,不能存图片/序列化数据
char* data = "abc\0def"; → strlen 返回 3,后面的 def 丢失
3. 拼接可能溢出 —— strcat 不检查目标缓冲区大小
char buf[10]; strcat(buf, "very long string"); → 缓冲区溢出
4. 截断不会释放 —— 字符串变短后,内存不会自动回收SDS 如何解决?
SDS(Redis 3.2+)结构:
struct sdshdr64 {
int64_t len; // 已用长度(字节),O(1) 获取
int64_t alloc; // 分配的总长度,不包括头和空终止符
unsigned char flags; // 低 3 位表示 SDS 类型(5/8/16/32/64 bit)
char buf[]; // 字符数组
};
buf 中存的是二进制安全的(以 len 字段为准,而非 '\0')
buf 末尾存 '\0' 是为了复用部分 C 函数(strcmp 等)二、原理拆解
2.1 SDS 5 种类型
为了节省内存,Redis 根据字符串长度选择不同头结构:
| 类型 | len/alloc 类型 | 适用长度 | 内存开销 |
|------|---------------|---------|---------|
| sdshdr5 | 不可用 | < 32 | 1B (flags) |
| sdshdr8 | uint8_t | < 256B | 3B |
| sdshdr16 | uint16_t | < 64KB | 5B |
| sdshdr32 | uint32_t | < 4GB | 9B |
| sdshdr64 | uint64_t | 不限 | 17B |
对于大部分小字符串,SDS 头只有 3 字节2.2 空间预分配策略
示例:
当前 len=10, 追加 5B:
预分配后 alloc=30 (10+5)*2=30
└── 多分配了 15B 的空间为将来准备
当前 len=1MB, 追加 100B:
预分配后 alloc≈1MB+1MB+100B
└── 翻倍到 2MB
**动机**:避免频繁 realloc,N 次追加操作的复杂度从 O(N²) 降到 O(N)。
### 2.3 惰性空间释放当字符串截断时(如 APPEND 后 SET 成小字符串),
SDS 并不立即释放多余内存(free 属性记录差值),
仅在需要时由 sdsRemoveFreeSpace 手动回收。
动机:字符串长度经常波动,避免频繁 malloc/free
### 2.4 String 的三种编码
```mermaid
flowchart TD
STRING["String (object type = OBJ_STRING)"]
INT["INT (整数)<br/>64位有符号整数直接存在 ptr 中"]
EMBSTR["EMBSTR (短串)<br/>SDS 头+数据<br/>一次 malloc 连续内存"]
RAW["RAW (长串)<br/>SDS 头+数据<br/>两次 malloc"]
STRING --> INT
STRING --> EMBSTR
STRING --> RAW
T1["维度 | INT | EMBSTR | RAW"]
V1["适用条件 | 整数表示 | ≤44字节 | >44字节"]
V2["内存分配 | 无(嵌入) | 1次 | 2次"]
V3["修改行为 | 转RAW | 转RAW | 正常修改"]
V4["内存局部性 | 极好 | 好 | 一般"]
V5["缓存友好性 | 最高 | 高 | 低"]
NOTE["EMBSTR上限44字节原因:<br/>Redis对象头(16B) + SDS头(3B) + 字符串(N) + \\0(1B) = 64B<br/>正好一个jemalloc/tcmalloc最小分配单位"]三、图解全景
四、实战验证
4.1 验证不同编码
redis-cli
# 1. INT 编码(数字)
127.0.0.1:6379> SET num 12345
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
# 2. EMBSTR 编码(短字符串)
127.0.0.1:6379> SET short "hello"
OK
127.0.0.1:6379> OBJECT ENCODING short
"embstr"
# 3. RAW 编码(长字符串)
127.0.0.1:6379> SET long "$(printf 'A%.0s' {1..50})"
OK
127.0.0.1:6379> STRLEN long
(integer) 50
127.0.0.1:6379> OBJECT ENCODING long
"raw"
# 4. 修改 embstr → 会转为 raw(EMBSTR 只读)
127.0.0.1:6379> APPEND short " world"
(integer) 11
127.0.0.1:6379> OBJECT ENCODING short
"raw"4.2 验证 SDS 性能优势
# SDS 的 O(1) 求长度性能测试
redis-cli
# 创建一个 1MB 的大字符串
127.0.0.1:6379> SET bigstring "$(dd if=/dev/zero bs=1M count=1 2>/dev/null | base64)"
OK
# STRLEN 是 O(1)
127.0.0.1:6379> STRLEN bigstring
(integer) 1396280 # 立即返回,不遍历
# 对比 C strlen 需要遍历 ~1.4MB4.3 内存占用分析
# 验证 SDS 不同大小头部的内存节省
# 大量小字符串
redis-benchmark -n 100000 SET key:1 value # 简单值
# 使用 redis-cli 查看内存
127.0.0.1:6379> INFO memory
# Memory
used_memory_human: # 观察内存总量
# 使用 redis-rdb-tools 分析内存
# rdbtools 可以解析 RDB 分析每个 key 的内存占用
pip install rdbtools
rdb -c memory dump.rdb --bytes 1024 | head -20五、面试视角
| 追问 | 答案要点 |
|------|---------|
| SDS 相比 C 字符串的优势? | O(1) 取长度、二进制安全、自动扩容(预分配)、惰性释放 |
| SDS 扩容的翻倍策略? | <1MB 翻倍(指数增长),>1MB 每次加 1MB(线性增长),避免大字符串浪费 |
| String 的三种编码? | INT(整数直接存)、EMBSTR(≤44B 一次分配)、RAW(>44B 两次分配)|
| 为什么 EMBSTR 上限 44 字节? | 与 64 字节内存分配单位对齐:16(robj)+3(sdshdr8)+44(data)+1(\0)=64 |
| SDS 是二进制安全的吗? | 是,以 len 字段为准而非 '\0',可存储图片/序列化/任意二进制 |
| SET int 存成 INT 编码后能做运算? | 是,INCR/DECR 等操作直接在 int 上运算,不转换编码 |
| APPEND 命令对编码的影响? | EMBSTR 被修改后自动转为 RAW,因为 EMBSTR 是只读的 |
📚 相关链接
**数据结构概览** — String 在整体中的位置
**内存优化** — SDS 类型选择如何影响内存
← 返回 **核心数据结构索引**