
前言这里是程序员阿亮在 Redis 中虽然底层是用 C 语言编写的但 Redis 并没有直接使用 C 语言传统的字符串表示以 \0 结尾的字符数组而是自己构建了一套名为SDSSimple 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 简化源码struct sdshdr { unsigned int len; // 已使用的长度buf 中已占用的字节数 unsigned int free; // 剩余可用的长度 char buf[]; // 实际存储数据的字符数组 };三、 为什么 Redis 必须定义 SDS1. 常数复杂度获取字符串长度由于 sdshdr 中直接维护了 len 字段Redis 获取键值的长度不需要遍历而是直接读取变量。对于一个极其高频的操作从 ON降到 O(1)是质的飞跃。2. 彻底杜绝缓冲区溢出SDS 的 API如 sdscat在进行字符串修改前会先检查 free 空间是否足够。如果空间不足SDS 会自动触发扩容然后再执行拼接操作。开发者不再需要手动计算内存安全性极高。3. 二进制安全Binary SafeSDS 不以 \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获取长度O(N)O(1)缓冲区安全不安全易溢出安全自动扩容内存重分配频繁每次修改必触发低频预分配 惰性释放二进制安全不支持遇到 \0 终止支持按 len 读取数据结尾以 \0 结尾以 \0 结尾为了兼容 C 函数结语Redis 为什么要定义 SDS答案很简单为了更快的速度、更强的稳定性和更广的应用场景。SDS 的设计体现了 Redis “空间换时间”的典型哲学。通过在内存结构中多增加几个字段Redis 成功规避了 C 语言字符串的历史包袱为其作为高性能内存数据库奠定了坚实的基础。