前言
这里是程序员阿亮
在 Redis 中,虽然底层是用 C 语言编写的,但 Redis 并没有直接使用 C 语言传统的字符串表示(以 \0 结尾的字符数组),而是自己构建了一套名为 SDS(Simple Dynamic String) 的抽象类型。
为什么作为"性能之王"的 Redis 要在最基础的数据结构上"大费周章"?本文将深入对比 C 原生字符串与 SDS,揭开 Redis 高性能背后的秘密。
一、C 语言原生字符串的"原罪"
Redis自定义我们的SDS主要原因就在于:C语言的字符串太弱,问题很多
在 C 语言中,字符串本质上是一个字符数组,并以空字符 \0 结尾。这种设计虽然简单,但在高性能、高并发的缓存系统中存在四大致命缺陷:
-
为了获取字符串长度,C 必须从头遍历直到遇到 \0。如果字符串很长,这个操作会非常耗时。时间复杂度是O(n)
-
非二进制安全 :
由于以 \0 标识结尾,意味着 C 字符串中间不能包含 \0。这导致它无法存储图片、音频或压缩文件等二进制数据。
-
容易导致缓冲区溢出(Buffer Overflow) :
使用 strcat 等函数拼接字符串时,如果忘记提前分配空间,会直接覆盖掉后续内存的数据,引发系统崩溃。
-
频繁的内存重分配 :
每次修改字符串长度,都必然涉及内存的重新分配(Realloc),这是一个非常重的系统调用。
二、SDS 的结构:它长什么样?
2.1 简化源码
cpp
struct sdshdr {
unsigned int len; // 已使用的长度(buf 中已占用的字节数)
unsigned int free; // 剩余可用的长度
char buf[]; // 实际存储数据的字符数组
};
三、 为什么 Redis 必须定义 SDS?
1. 常数复杂度获取字符串长度
由于 sdshdr 中直接维护了 len 字段,Redis 获取键值的长度不需要遍历,而是直接读取变量。对于一个极其高频的操作,从 O(N)降到 O(1)是质的飞跃。
2. 彻底杜绝缓冲区溢出
SDS 的 API(如 sdscat)在进行字符串修改前,会先检查 free 空间是否足够。如果空间不足,SDS 会自动触发扩容,然后再执行拼接操作。开发者不再需要手动计算内存,安全性极高。
3. 二进制安全(Binary Safe)
SDS 不以 \0 作为结束标志,而是严格根据 len 字段来决定字符串在哪里结束。
- 意义:Redis 不仅能存文本,还能直接存储 Protobuf 序列化数据、视频流分片、加密后的二进制密文等任何内容。
4. 空间预分配与惰性空间释放(核心性能优化)
这是 Redis 减少系统调用的核心手段:
-
空间预分配(Optimistic Expansion) :
当对 SDS 进行增长操作时,程序不仅会分配必要的空间,还会多分配额外的未使用空间。
-
如果修改后 len < 1MB,则分配 len 同样大小的 free 空间(翻倍)。
-
如果修改后 len ≥ 1MB,则固定分配 1MB 的 free 空间。
-
效果 :将连续增长 N 次字符串所需的内存分配次数,从 N 次降低到了最多 N 次。
-
-
惰性空间释放(Lazy Freeing) :
当缩短字符串时,Redis 并不立即释放内存,而是增加 free 的值。
-
效果:避免了内存缩容的开销,并为未来的增长操作做好了准备。
四、 总结:C 字符串 vs Redis SDS
|-----------|-----------------|---------------------|
| 特性 | C 语言原生字符串 | Redis SDS |
| 获取长度 | cpp O(N) | cpp O(1) |
| 缓冲区安全 | 不安全(易溢出) | 安全(自动扩容) |
| 内存重分配 | 频繁(每次修改必触发) | 低频(预分配 + 惰性释放) |
| 二进制安全 | 不支持(遇到 \0 终止) | 支持(按 len 读取) |
| 数据结尾 | 以 \0 结尾 | 以 \0 结尾(为了兼容 C 函数) |
结语
Redis 为什么要定义 SDS?
答案很简单:为了更快的速度、更强的稳定性和更广的应用场景。
SDS 的设计体现了 Redis "空间换时间"的典型哲学。通过在内存结构中多增加几个字段,Redis 成功规避了 C 语言字符串的历史包袱,为其作为高性能内存数据库奠定了坚实的基础。