在 Redis 的源码中,有一个非常核心且常被面试官问到的数据结构——SDS(Simple Dynamic String,简单动态字符串)。Redis 没有直接使用 C 语言原生的字符串(以空字符 \0 结尾的字符数组),而是自己实现了一套动态字符串。这背后有非常充分的工程考量,也是理解 Redis 高性能设计的一个绝佳切入点。
如果你去面试 Redis 相关岗位,这个问题几乎必问。本文将从 C 字符串的缺陷出发,逐一分析 SDS 的设计优势,并给出面试中如何回答的建议。
一、C 字符串的先天不足
C 语言中的字符串本质上是一个以 \0 结尾的 char 数组。这种设计在系统编程中很常见,但在 Redis 这种需要频繁操作字符串、对性能和安全性要求极高的场景下,暴露出几个致命问题。
1. 获取长度的时间复杂度为 O(n)
C 字符串没有保存自身长度,想得到字符串长度必须从头遍历直到遇到 \0。对于一个长度为 n 的字符串,strlen 的时间复杂度是 O(n)。
Redis 中很多操作都需要频繁获取字符串长度,比如 STRLEN 命令、追加、截断等。如果每次都是 O(n),在长字符串场景下性能会急剧下降。
2. 非二进制安全
C 字符串以 \0 作为结束标志,这意味着字符串中间不能包含 \0。但 Redis 经常需要存储二进制数据,比如图片、序列化后的对象、压缩数据等,这些数据中完全可能出现 \0 字节。
如果直接用 C 字符串,遇到 \0 就会被截断,导致数据丢失或读取错误。这是 Redis 作为数据库绝对不能接受的。
3. 缓冲区溢出风险
C 字符串不记录自身容量。当你执行 strcat 拼接时,如果目标缓冲区空间不足,就会发生缓冲区溢出,可能覆盖相邻内存,造成程序崩溃甚至安全漏洞。
Redis 需要频繁进行字符串拼接(如 APPEND 命令),如果依赖 C 字符串,必须由调用者手动保证空间足够,这在工程上既繁琐又危险。
4. 修改字符串需要频繁内存重分配
C 字符串每次增长或缩短,都需要调用 realloc 重新分配内存。对于增长操作,如果忘记分配,就会溢出;对于缩短操作,如果不释放多余空间,就会造成内存泄漏。
Redis 中字符串修改非常频繁,如果每次修改都重分配内存,性能开销巨大,还会产生大量内存碎片。
二、SDS 的设计与优势
SDS 是 Redis 自己封装的字符串结构。以 Redis 3.2 之后的版本为例,SDS 根据字符串长度分为 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64 五种类型,目的是用不同大小的头部来节省内存。
以 sdshdr8 为例,其结构大致如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 已使用长度
uint8_t alloc; // 总分配长度(不含头部和结尾)
unsigned char flags; // 类型标志
char buf[]; // 柔性数组,实际存储字符串
};
SDS 针对 C 字符串的缺陷,做了以下改进。
1. O(1) 获取字符串长度
SDS 在头部用 len 字段直接保存了字符串长度。获取长度只需读取这个字段,时间复杂度 O(1)。
这使得 STRLEN 命令、字符串追加、截断等操作都能快速完成,不再需要遍历整个字符串。
2. 二进制安全
SDS 不依赖 \0 来判断字符串结束,而是用 len 字段记录长度。因此字符串中间可以包含任意 \0 字节,完全支持二进制数据。
SDS 的 buf 数组末尾仍然会加上一个 \0,但这只是为了兼容部分 C 字符串函数(如 printf),并不作为结束标志。
3. 杜绝缓冲区溢出
SDS 在修改字符串前,会先检查 alloc - len 是否足够。如果不够,会自动扩容,然后再执行拼接操作。这样调用者无需手动管理空间,从根本上避免了缓冲区溢出。
4. 空间预分配与惰性释放
这是 SDS 在性能上最精妙的设计之一。
空间预分配:当 SDS 需要扩容时,不仅会分配所需空间,还会额外分配一些冗余空间。
- 如果修改后
len小于 1MB,那么alloc会分配为len * 2,即多分配一倍空间。 - 如果修改后
len大于等于 1MB,那么alloc会分配为len + 1MB,即多分配 1MB。
这样下次追加时,如果空间足够,就无需再次分配内存。通过这种策略,SDS 将连续增长操作的内存重分配次数从 N 次降低到最多 N 次中的少数几次,均摊时间复杂度大大降低。
惰性释放:当 SDS 缩短字符串时,程序不立即释放多余内存,而是修改 len 字段,保留这部分空间以备将来使用。当然,SDS 也提供了真正的释放接口,在需要时才会调用。
这种“预分配 + 惰性释放”的组合,让 Redis 在频繁修改字符串时,既减少了内存分配次数,又避免了频繁释放带来的开销。
5. 兼容部分 C 字符串函数
由于 SDS 的 buf 末尾仍然保留 \0,所以可以直接复用一部分 C 标准库函数,比如 strlen、strcat 等(虽然 Redis 内部通常用自己的实现)。这降低了代码迁移和复用的成本。
三、面试如何回答
如果面试官问“Redis 的 SDS 为什么不用 C 字符串”,你可以按以下逻辑组织回答:
- 先点明 C 字符串的四个缺陷:获取长度 O(n)、非二进制安全、缓冲区溢出、频繁内存重分配。
- 再对应 SDS 的四个优势:O(1) 长度获取、二进制安全、自动扩容防溢出、空间预分配与惰性释放。
- 最后补充一个亮点:SDS 根据长度选择不同头部类型,兼顾内存效率和灵活性;末尾保留
\0以兼容 C 函数。
这样回答既有广度又有深度,能体现出你对 Redis 底层设计的理解,而不仅仅是背诵结论。
四、总结
Redis 不用 C 字符串,不是因为 C 字符串“不好”,而是因为 Redis 的使用场景对字符串操作有更高要求:高性能、二进制安全、内存可控、防止溢出。SDS 通过增加少量元数据,换来了 O(1) 长度获取、自动扩容、空间预分配和惰性释放等能力,是典型的“以空间换时间、以封装换安全”的工程实践。
理解 SDS 的设计,不仅有助于回答面试题,更能让你在写代码时思考:数据结构的选择,永远要服务于具体的业务场景和性能目标。
未经允许不得转载:任鹏个人博客 » Redis 的 SDS 为什么不用 C 字符串

