我们来详细解析一下 Redis 的核心数据结构之一:
sdshdr
。
sdshdr
是 “Simple Dynamic String header” 的缩写,意为“简单动态字符串头”。它是在 Redis 自己实现的字符串库(SDS)中,用于定义字符串对象的头部结构。理解了 sdshdr
,就能明白为什么 Redis 的字符串操作如此高效和安全。
简单来说,sdshdr
是 Redis 字符串(SDS)的元数据部分,它紧邻实际的字符串数据存放在同一块连续的内存中,记录了字符串的长度、空余空间等信息。
为什么 Redis 不直接使用 C 语言的字符串?
要理解 sdshdr
的重要性,首先要明白传统 C 语言字符串(以 \0
结尾的字符数组)的缺陷:
- 获取长度效率低: C 语言字符串本身不记录长度,要获取其长度必须遍历整个字符串,直到遇到
\0
,时间复杂度为 O ( N ) O(N) O(N)。 - 容易造成缓冲区溢出(Buffer Overflow): 当使用
strcat
等函数拼接字符串时,如果目标数组空间不足,就会发生缓冲区溢出,这是一种严重的安全漏洞。 - 二进制不安全: C 语言字符串以
\0
(空字符) 作为结束符,这意味着字符串内容不能包含\0
。因此,无法用它来存储图片、音频等二进制数据。 - 内存管理复杂: 每次增长或缩短字符串,都需要手动进行复杂的内存重分配,容易出错且效率不高。
为了解决这些问题,Redis 设计了 SDS。而 SDS 的核心就是 sdshdr
头部结构。
sdshdr 的结构
一个完整的 SDS 字符串在内存中由两部分组成:
- 头部(Header): 即
sdshdr
结构体。 - 数据(Data): 紧跟在头部后面的实际字符串内容。
sdshdr
并不是一个单一的结构,为了节省内存,Redis 根据字符串的实际长度,定义了多种不同的 sdshdr
类型(在 sds.h
源码中定义)。
在 Redis 5.0 及以后的版本中,sdshdr
的通用结构可以看作是:
struct __attribute__ ((__packed__)) sdshdr<T> {
T len; // 已使用长度 (length of the string)
T alloc; // 总分配长度 (total allocated length, excluding header and null terminator)
unsigned char flags; // 标志位 (flags, indicating the header type)
char buf[]; // 柔性数组 (flexible array member), 代表实际的字符串数据
};
关键字段解释:
len
: 记录了buf
中已存储字符串的实际长度。有了它,Redis 获取字符串长度的时间复杂度是 O ( 1 ) O(1) O(1),极其高效。alloc
: 记录了不包括头部和末尾\0
的情况下,总共为buf
分配的内存空间大小。len
和alloc
的差值就是剩余可用空间。flags
: 一个3位的字段,用来表示当前sdshdr
的具体类型。buf[]
: 这是一个“柔性数组成员”,是 C99 的一个特性。它表示buf
指向sdshdr
结构体之后紧跟的内存地址,这里存放着实际的字符串内容。字符串的末尾同样会追加一个\0
,以兼容部分 C 语言函数库。
__attribute__ ((__packed__))
是一个 GCC 的指令,用于告诉编译器取消结构体在编译过程中的内存对齐优化,使得结构体成员紧凑排列,从而节省内存。
sdshdr 的不同类型
根据 flags
字段的值,Redis 会使用不同的 sdshdr
结构,主要区别在于 len
和 alloc
字段的数据类型,从而节省头部占用的空间:
flags 值 |
类型 | len 和 alloc 的数据类型 |
头部大小 |
---|---|---|---|
0 | sdshdr5 |
(没有 len /alloc 字段) |
1 字节 |
1 | sdshdr8 |
uint8_t (8位无符号整数) |
3 字节 |
2 | sdshdr16 |
uint16_t (16位无符号整数) |
5 字节 |
3 | sdshdr32 |
uint32_t (32位无符号整数) |
9 字节 |
4 | sdshdr64 |
uint64_t (64位无符号整数) |
17 字节 |
特别说明 sdshdr5
:
sdshdr5
是一个特例,它没有 len
和 alloc
字段。它的 flags
字段本身就编码了字符串的长度(高5位存长度,低3位存类型)。它只能用于存储非常短的字符串。
Redis 会根据字符串的长度自动选择最小的、能容纳该字符串的 sdshdr
类型,实现极致的内存优化。
sdshdr 带来的优势总结
基于 sdshdr
结构,Redis 的 SDS 相比 C 语言字符串获得了巨大优势:
- 常数时间复杂度的长度获取: 直接读取
len
属性即可,时间复杂度为 O ( 1 ) O(1) O(1)。 - 杜绝缓冲区溢出: 当对 SDS 进行修改时(如
APPEND
),SDS 的 API 会先检查alloc - len
的剩余空间是否足够。如果不足,它会自动进行内存重分配,扩展buf
的大小,然后再执行操作,从而保证了安全。(对这里len和alloc不理解的可以看文章末尾!!!) - 空间预分配与惰性释放(减少内存重分配次数):
- 空间预分配: 当对 SDS 进行扩展时,如果修改后字符串长度小于 1MB,程序会分配
len * 2
的空间;如果超过 1MB,则会额外多分配 1MB 的空间。这种策略避免了每次增加字符串都重新分配内存,提升了性能。 - 惰性空间释放: 当缩短 SDS 字符串时,程序并不会立即释放多出来的空间,而是更新
len
字段,将这部分空间记录为未使用,以备将来再次使用。
- 空间预分配: 当对 SDS 进行扩展时,如果修改后字符串长度小于 1MB,程序会分配
- 二进制安全: SDS 使用
len
属性来判断字符串结束,而不是\0
。因此buf
中可以包含任意字符,包括\0
。这使得 SDS 可以安全地存储任何二进制数据。 - 兼容部分 C 语言函数: SDS 字符串的末尾依然保留了一个
\0
字符(这个\0
不计入len
长度),这使得那些只读取而不修改字符串的 C 语言函数(如printf
、strcmp
)可以直接处理 SDS 的buf
部分。
综上所述,sdshdr
是 Redis 高性能字符串实现的关键基石。它通过一个精巧的头部设计,解决了传统 C 语言字符串的诸多痛点,为 Redis 提供了高效、安全且功能丰富的字符串处理能力。