Skip to content

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 验证不同编码

bash

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 性能优势

bash

# 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.4MB

4.3 内存占用分析

bash

# 验证 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 类型选择如何影响内存

  • ← 返回 **核心数据结构索引**

Knowledge4J — Java 知识库