目录
[1. 引言](#1. 引言)
[1.1 Redis 中字符串的广泛应用](#1.1 Redis 中字符串的广泛应用)
[2. SDS 结构定义](#2. SDS 结构定义)
[2.1 Redis 3.2 之前的 SDS 结构](#2.1 Redis 3.2 之前的 SDS 结构)
[2.2 Redis 3.2 及之后的 SDS 结构](#2.2 Redis 3.2 及之后的 SDS 结构)
[3. SDS 与传统 C 字符串的比较](#3. SDS 与传统 C 字符串的比较)
[3.1 获取字符串长度](#3.1 获取字符串长度)
[3.2 缓冲区溢出问题](#3.2 缓冲区溢出问题)
[3.3 二进制安全性](#3.3 二进制安全性)
[3.4 内存分配次数](#3.4 内存分配次数)
[4. SDS 的内存分配策略](#4. SDS 的内存分配策略)
[4.1 空间预分配](#4.1 空间预分配)
[4.2 惰性空间释放](#4.2 惰性空间释放)
[5. SDS 的其他特性](#5. SDS 的其他特性)
[5.1 兼容 C 字符串函数](#5.1 兼容 C 字符串函数)
[5.2 类型灵活](#5.2 类型灵活)
[6. SDS 的使用场景](#6. SDS 的使用场景)
[6.1 键值对存储](#6.1 键值对存储)
[6.2 数据缓存](#6.2 数据缓存)
[6.3 日志系统](#6.3 日志系统)
[6.4 发布/订阅](#6.4 发布/订阅)
[7. 总结](#7. 总结)
1. 引言
在 Redis 中,SDS(Simple Dynamic String,简单动态字符串)是一种用于替代传统 C 语言字符串的数据结构。作为一个高性能的内存数据库,Redis 频繁进行字符串操作,而传统 C 字符串在性能和安全性上存在诸多问题。为此,Redis 专门设计了 SDS,旨在满足更高效的字符串存储和操作需求。
1.1 Redis 中字符串的广泛应用
在 Redis 中,字符串(String)是最基础、最常用的数据类型之一。其应用场景包括:
- 缓存数据(如 HTML 页面片段、用户会话信息)
- 计数器(如网站访问量、点赞数)
- 位图(Bitmap) 和 地理位置(GEO)数据 等底层数据结构的实现
- 发布/订阅(Pub/Sub) 、流水线(Pipeline) 等功能的数据交互
因此,优化字符串的存储和操作效率对 Redis 性能至关重要。
2. SDS 结构定义
2.1 Redis 3.2 之前的 SDS 结构
在 Redis 3.2 之前,SDS 的结构较为简单,定义如下:
cs
struct sdshdr {
int len; // 字符串的实际长度
int free; // 未使用的字节数
char buf[]; // 字符数组,末尾以 '\0' 兼容 C 字符串函数
};
解释:
len
:存储字符串当前的长度(不包括\0
)。free
:记录buf
数组中未使用的空间,便于后续字符串增长。buf[]
:存储字符串数据,末尾保留\0
以兼容 C 标准库字符串函数。
2.2 Redis 3.2 及之后的 SDS 结构
Redis 3.2 及更高版本对 SDS 进行了优化,采用了多种 SDS 结构以适应不同长度的字符串。以下是几种 SDS 类型的定义:
cs
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 低 3 位表示类型,高 5 位表示长度 */
char buf[]; // 适用于长度 < 32 字节的字符串
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[]; // 适用于长度 < 256 字节的字符串
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[]; // 适用于长度 < 65536 字节的字符串
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[]; // 适用于长度 < 2^32 字节的字符串
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[]; // 适用于长度 ≥ 2^32 字节的字符串
};
关键点:
flags
字段的低 3 位标识 SDS 类型(如sdshdr8
、sdshdr16
等),高 5 位用于记录sdshdr5
类型的字符串长度。- SDS 根据字符串长度选择合适的结构体类型,减少内存浪费,提升存储效率。
3. SDS 与传统 C 字符串的比较
3.1 获取字符串长度
- 传统 C 字符串 :需遍历字符串直到遇到
\0
,时间复杂度为 O(n) - SDS :直接通过
len
字段获取长度,时间复杂度为 O(1)
3.2 缓冲区溢出问题
- 传统 C 字符串 :
strcat()
等函数可能因缓冲区空间不足导致溢出。 - SDS :每次字符串修改前都会检查
free
字段中的剩余空间,若不足则自动扩展,避免溢出风险。
3.3 二进制安全性
- 传统 C 字符串 :以
\0
作为结束标志,无法存储带有\0
的二进制数据。 - SDS :使用
len
记录实际长度,允许存储任意二进制数据。
3.4 内存分配次数
- 传统 C 字符串:每次修改都会重新分配内存,效率较低。
- SDS :采用 空间预分配 和 惰性空间释放 策略,减少内存重分配次数,提高性能。
4. SDS 的内存分配策略
4.1 空间预分配
- 如果修改后的字符串长度 小于 1MB ,则分配内存为
原长度的两倍 + 1
字节。 - 如果修改后的字符串长度 大于等于 1MB ,则分配内存为
原长度 + 1MB + 1
字节。
示例:
- 原字符串
hello
,长度为 5。 append(" world")
后,字符串变为"hello world"
,长度 11。- 此时,SDS 会分配 22 个字节的内存,满足空间预分配策略(
11 * 2 + 1 = 22
)。
4.2 惰性空间释放
当 SDS 进行截断等缩短操作时,并不会立即释放多余内存,而是将其保留在 free
字段中,供后续使用。这种策略减少了频繁的内存分配操作,进一步提升了性能。
5. SDS 的其他特性
5.1 兼容 C 字符串函数
SDS 在 buf
数组末尾保留了 \0
,确保兼容如 strlen()
、strcat()
等 C 标准库字符串函数,降低了代码迁移的难度。
5.2 类型灵活
Redis 根据字符串长度选择最适合的 SDS 结构体:
sdshdr8
适用于短字符串,节省内存。sdshdr64
适用于超长字符串,满足大数据量场景需求。
6. SDS 的使用场景
6.1 键值对存储
Redis 的 SET
、GET
命令广泛使用 SDS,快速获取字符串长度,提高查询效率。
6.2 数据缓存
SDS 的预分配机制减少了频繁的内存分配操作,特别适用于缓存场景。
6.3 日志系统
SDS 的二进制安全特性可存储多种格式的数据,是日志系统中的理想选择。
6.4 发布/订阅
在 Pub/Sub 场景中,SDS 可灵活地处理多种数据流格式,确保消息高效传递。
7. 总结
SDS 作为 Redis 专门设计的字符串结构,解决了传统 C 字符串在性能、安全性和内存管理方面的问题。
- 通过 空间预分配 和 惰性空间释放 策略,SDS 有效减少了内存分配次数,提高了字符串操作的性能。
- SDS 兼容 C 标准库字符串函数,具备出色的灵活性和高效性。
在 Redis 的高性能环境下,SDS 作为底层字符串结构,充分展现了其强大优势,是 Redis 高效运作的核心之一。