Redis里面什么是sdshdr,可以详细介绍一下吗?
文章目录
- 为什么 Redis 不直接使用 C 语言的字符串?
- sdshdr 的结构
- sdshdr 的不同类型
- sdshdr 带来的优势总结
我们来详细解析一下 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 提供了高效、安全且功能丰富的字符串处理能力。
下一篇:
Redis中的sdshdr的len和alloc那块的知识点详解
