深度解构 Redis 核心:为什么自研 SDS 才是内存数据库的“财富密码”?
前言
在 Redis 的世界里,哪怕是一个简单的 SET hello world,底层也并不简单。很多人知道 Redis 快,是因为它在内存中运行,但很少有人意识到,Redis 对内存中每一比特的“吝啬”和对性能的极致压榨,很大程度上归功于其自研的动态字符串——SDS (Simple Dynamic String)。
本文将结合 Redis 源码与底层设计逻辑,带你拆解 SDS 是如何解决 C 原生字符串的“痛点”,并进化为数据库级基础设施的。
一、 C 语言字符串:高性能系统的“阿喀琉斯之踵”
C 语言本身并没有独立的“字符串”类型,它本质上只是一个以 \0(NULL 字符)结尾的字符数组(Capture Array)。
1. 结构复现
正如课上所讲,声明一个 char *s = "hello",在内存中实际上是:
| h | e | l | l | o | \0 |
这种设计虽然简单,但在数据库场景下有三大“死穴”:
- 获取长度的代价高达:
由于 C 字符串不记录长度,调用strlen()必须从头遍历直到遇见\0。在 Redis 这种高频统计长度的系统中,这会产生巨大的性能浪费。 - 二进制安全(Binary Safe)危机:
C 字符串以\0为结束标识。如果你的数据里本身就包含\0(比如图片、压缩包、Protobuf 数据),C 语言会认为字符串到一半就结束了。 - 缓冲区溢出(Buffer Overflow)风险:
原生 C 字符串不记录空余容量。执行拼接操作(如strcat)时,如果没提前申请足够空间,就会直接覆盖邻近内存,导致系统崩溃或被黑客利用。
二、 SDS 的逆袭:结构体里的艺术
为了解决上述问题,Redis 自己构建了一套结构体。你可以把它理解为 Java 中的一个类,里面包含了多个成员变量来管理数据。
1. 核心属性拆解
以最经典的 sdshdr8(用于 255 字节以内的字符串)为例,其源码结构如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用字节数(字符串真实长度) */
uint8_t alloc; /* 申请的总空间(不含头和结束符) */
unsigned char flags; /* 标志位,代表 SDS 的类型(8/16/32/64等) */
char buf[]; /* 真正的字节数组,存放数据 */
};
len(已用长度):让获取长度的操作直接降级为 。alloc(已分配容量):这个字段是实现“空间预分配”的关键。它告诉 Redis:我总共申请了多少地盘,目前还剩多少空闲。buf[](缓冲区):- 二进制安全:SDS 通过
len判断数据结束,而不是\0,因此buf里可以放任何特殊字符。 - 兼容性:即使是 SDS,
buf末尾依然会自动加一个\0。为什么要多此一举?为了让 SDS 能直接调用 C 语言的标准函数库(如printf),不用重复造轮子。
三、 极致抠门:SDS 的“阶梯式”Header 设计
这是 Redis 源码中最具匠心的地方。Redis 开发者意识到:并不是所有字符串都需要用相同的 Header。
如果一个 Key 只有 5 个字节,你却用 uint64_t 来存它的长度,那么 Header 占用的空间(16 字节)甚至比数据本身还大!
SDS 的五种形态
Redis 3.2 之后,根据字符串长度自动选择不同的 Header:
| 类型 | Header 大小 | 适用场景 | 内存节省逻辑 |
|---|---|---|---|
sdshdr5 | 1 字节 | 极短字符串 | 连 len 和 alloc 都省了,长度塞进 flags 的前 5 位。 |
sdshdr8 | 3 字节 | < 255 字节 | 使用 uint8_t (1字节) 记录长度。 |
sdshdr16 | 5 字节 | < 64KB | 使用 uint16_t (2字节) 记录长度。 |
sdshdr32 | 9 字节 | < 4GB | 使用 uint32_t (4字节)。 |
sdshdr64 | 17 字节 | 海量数据 | 使用 uint64_t (8字节)。 |
结论: Redis 会根据内容长度“自动升级”Header。这种“按需分配”的设计,让 Redis 在处理数亿个 Key 时,能省下几个 GB 的昂贵内存。
四、 空间预分配策略:别让 realloc 拖慢速度
在 Redis 中,字符串的修改(如 APPEND)非常频繁。如果每次修改都去系统申请内存(realloc),性能会因为频繁的内核态调用而暴跌。
SDS 采用了空间预分配策略:
- 小于 1MB:修改后如果需要扩容,Redis 会直接给双倍空间。
- 大于 1MB:修改后如果需要扩容,Redis 每次多给 1MB 的冗余空间。
这样,很多连续的修改操作就能在当前 alloc 范围内完成,无需频繁申请内存。
五、 总结:从语言工具到基础设施
通过对 SDS 的剖析,我们可以看到 Redis 的设计哲学:
- 性能至上:变是质的飞跃。
- 安全可靠:彻底杜绝缓冲区溢出和二进制截断。
- 内存敏感:五种 Header 类型把内存压榨到了极致。
- 工程友好:向后兼容 C 原生函数。
在 Redis 的架构中,所有的键(Key)和绝大部分的值(Value)最终都会封装进一个叫 RedisObject 的结构体中,而其底层的灵魂正是 SDS。没有 SDS,Redis 就不可能在保证高性能的同时,还能如此优雅地处理多样的二进制数据。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_83144588/article/details/157735425




