Redis SDS(Simple Dynamic String)技术文档
一、概述
SDS(Simple Dynamic String)是 Redis 自研的动态字符串实现 ,用于替代 C 语言标准库的 char* 字符串。它是 Redis 最基础的数据结构之一,几乎所有字符串操作都基于 SDS。
1.1 为什么需要 SDS?
C 语言原生字符串存在以下问题:
| 问题 | C 字符串 | SDS |
|---|---|---|
| 获取长度 | O(n),需遍历到 \0 |
O(1),直接读取 len 字段 |
| 缓冲区溢出 | 容易发生 | 自动扩容,安全 |
| 二进制安全 | 不支持(遇到 \0 就结束) |
支持(内部可包含 \0) |
| 内存重新分配 | 每次修改都可能 realloc | 预分配策略,减少次数 |
| 兼容性 | - | 完全兼容 C 字符串函数 |
二、核心设计思想
2.1 五大特性
- 兼容 C 字符串 :以
\0结尾,可直接传给printf()、strlen()等标准库函数 - O(1) 获取长度 :头部保存
len字段,无需遍历 - 自动扩容:写入前检查容量,避免缓冲区溢出
- 预分配策略:扩容时多分配空间,减少后续重新分配次数
- 二进制安全 :字符串内部可包含
\0,不依赖其作为结束标志
2.2 内存布局
lua
以 sdshdr8 为例:
低地址 ------------------------------------> 高地址
+----------------------------------------+--------+--------+--------+------+
| len | alloc | flags | 'h' | 'e' | ... |
| (1字节) | (1字节) |(1字节) | | | |
+----------+----------+--------+--------+--------+------+
^
sds 指针指向这里
关键点:
sds指针指向的是buf[0],而不是头部- 通过
s[-1]可以访问flags字段 - 通过
s - sizeof(struct sdshdr8)可以得到头部地址
三、数据结构定义
3.1 五种头部结构体
Redis 根据字符串长度选择不同的头部类型,以节省内存:
c
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3位类型 + 5位长度 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 当前长度(最大 255)*/
uint8_t alloc; /* 总分配空间 */
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 最大 65535 */
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 约 4GB */
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 超大 */
uint64_t alloc;
unsigned char flags;
char buf[];
};
3.2 类型选择规则
| 类型 | 最大字符串长度 | 头部大小 | 适用场景 |
|---|---|---|---|
sdshdr5 |
31 | 1 字节 | 极短字符串(实际很少用) |
sdshdr8 |
255 | 3 字节 | 短字符串(最常见) |
sdshdr16 |
65,535 | 5 字节 | 中等长度 |
sdshdr32 |
~4GB | 10 字节 | 长字符串 |
sdshdr64 |
超大 | 18 字节 | 超长字符串 |
选择逻辑 (sds.c:33-43):
c
char sdsReqType(size_t string_size) {
if (string_size < 1 << 5) return SDS_TYPE_5;
if (string_size <= (1 << 8) - sizeof(struct sdshdr8) - 1) return SDS_TYPE_8;
if (string_size <= (1 << 16) - sizeof(struct sdshdr16) - 1) return SDS_TYPE_16;
// ... 依此类推
}
3.3 __attribute__((__packed__)) 的作用
告诉编译器不要填充字节,确保结构体成员紧密排列:
less
正常情况(有填充):
[len][alloc][flags][pad][pad][pad][buf...] // 浪费 3 字节
Packed(无填充):
[len][alloc][flags][buf...] // 紧凑排列
四、Flags 字段详解
4.1 Flags 的双重作用
flags 是一个 unsigned char(1 字节),包含两部分信息:
lua
Bit: 7 6 5 4 3 2 1 0
|--- ---|--- ---|
unused type mark
(5 bits) (3 bits)
对于 sdshdr5(特殊编码):
lua
Bit: 7 6 5 4 3 2 1 0
|--- ---|--- ---|
string length type mark
(5 bits) (3 bits)
4.2 类型常量定义
c
#define SDS_TYPE_5 0 /* 000 */
#define SDS_TYPE_8 1 /* 001 */
#define SDS_TYPE_16 2 /* 010 */
#define SDS_TYPE_32 3 /* 011 */
#define SDS_TYPE_64 4 /* 100 */
#define SDS_TYPE_MASK 7 /* 111 --- 取低 3 位 */
#define SDS_TYPE_BITS 3
4.3 如何读取类型
c
static inline unsigned char sdsType(sds s) {
unsigned char flags = s[-1]; // s[-1] 就是 flags
return flags & SDS_TYPE_MASK; // 取低 3 位
}
4.4 辅助位(Aux Bits)
除了低 3 位的类型标记,flags 的高 5 位还可以存储用户自定义数据:
c
// 设置辅助位(bit 索引 0-4)
void sdsSetAuxBit(sds s, int bit, int value);
// 获取辅助位
int sdsGetAuxBit(sds s, int bit);
用途:用于特殊场景(如嵌入到其他结构中的不可变 SDS)。
五、核心 API
5.1 创建与销毁
创建字符串
c
// 从指定内容创建
sds sdsnew(const char *init);
sds sdsnewlen(const void *init, size_t initlen);
// 尝试创建(失败返回 NULL,不崩溃)
sds sdstrynewlen(const void *init, size_t initlen);
// 创建空字符串
sds sdsempty(void);
// 复制现有 SDS
sds sdsdup(const sds s);
// 在预分配内存中初始化(类似 C++ placement new)
sds sdsnewplacement(char *buf, size_t bufsize, char type,
const char *init, size_t initlen);
特殊用法:跳过初始化
c
const char *SDS_NOINIT = "SDS_NOINIT";
// 创建但不初始化内容(性能优化)
sds s = sdsnewlen(SDS_NOINIT, 1024);
memcpy(s, my_data, 1024); // 自己填内容
释放字符串
c
void sdsfree(sds s);
void sdsfreegeneric(void *s); // 通用版本
5.2 查询操作
c
// 获取长度(O(1))
size_t sdslen(const sds s);
// 获取可用空间
size_t sdsavail(const sds s);
// 获取总分配空间
size_t sdsalloc(const sds s);
// 比较两个 SDS
int sdscmp(const sds s1, const sds s2);
5.3 修改操作
拼接
c
// 拼接普通字符串
sds sdscat(sds s, const char *t);
// 拼接指定长度的字符串(支持二进制数据)
sds sdscatlen(sds s, const void *t, size_t len);
// 拼接另一个 SDS
sds sdscatsds(sds s, const sds t);
// 格式化拼接(类似 sprintf)
sds sdscatprintf(sds s, const char *fmt, ...);
复制
c
sds sdscpy(sds s, const char *t);
sds sdscpylen(sds s, const char *t, size_t len);
清空
c
void sdsclear(sds s); // 保留内存,只重置长度
5.4 高级操作
分割
c
// 按分隔符分割
sds *sdssplitlen(const char *s, ssize_t len,
const char *sep, int seplen, int *count);
// 解析命令行参数
sds *sdssplitargs(const char *line, int *argc);
// 释放分割结果
void sdsfreesplitres(sds *tokens, int count);
转换
c
// 转小写/大写
void sdstolower(sds s);
void sdstoupper(sds s);
// 数字转字符串
sds sdsfromlonglong(long long value);
模板替换
c
typedef sds (*sdstemplate_callback_t)(const sds variable, void *arg);
sds sdstemplate(const char *template, sdstemplate_callback_t cb_func, void *cb_arg);
六、底层机制
6.1 自动扩容
当需要追加内容时,SDS 会自动检查容量并扩容:
c
sds sdsMakeRoomFor(sds s, size_t addlen);
扩容策略 ([sds.c 中的实现逻辑】:
c
if (addlen <= avail) {
// 剩余空间足够,不需要扩容
return s;
}
// 计算新长度
size_t newlen = len + addlen;
if (newlen < SDS_MAX_PREALLOC) {
// 小于 1MB:双倍扩容
newlen *= 2;
} else {
// 大于等于 1MB:额外增加 1MB
newlen += SDS_MAX_PREALLOC;
}
预分配常量:
c
#define SDS_MAX_PREALLOC (1024*1024) // 1MB
6.2 内存分配器抽象
SDS 不直接使用 malloc/free,而是通过 sdsalloc.h 映射到 Redis 的 zmalloc 系列函数:
c
#include "zmalloc.h"
#define s_malloc zmalloc
#define s_realloc zrealloc
#define s_free zfree
#define s_malloc_usable zmalloc_usable
// ... 等等
好处:
- 统一内存管理(统计、泄漏检测)
- 支持切换分配器(jemalloc/tcmalloc)
- 获取实际可用空间(
*_usable函数)
6.3 类型自适应调整
由于内存分配器(如 jemalloc)可能返回比请求更大的空间,SDS 会在创建时自动升级类型:
c
static inline int adjustTypeIfNeeded(char *type, int *hdrlen, size_t bufsize) {
size_t usable = bufsize - *hdrlen - 1;
if (*type != SDS_TYPE_5 && usable > sdsTypeMaxSize(*type)) {
*type = sdsReqType(usable); // 升级到更大的类型
*hdrlen = sdsHdrSize(*type);
return 1;
}
return 0;
}
七、使用示例
7.1 基本用法
c
#include "sds.h"
// 创建
sds s = sdsnew("Hello");
printf("%s\n", s); // 输出: Hello
printf("len=%zu\n", sdslen(s)); // 输出: len=5
// 拼接
s = sdscat(s, " World");
printf("%s\n", s); // 输出: Hello World
// 释放
sdsfree(s);
7.2 高性能批量拼接
c
// 错误做法:每次都 realloc
sds s = sdsempty();
for (int i = 0; i < 1000; i++) {
s = sdscatprintf(s, "%d ", i); // 频繁 realloc
}
// 正确做法:预先分配足够空间
sds s = sdsnewlen(SDS_NOINIT, 10000); // 预分配 10KB
int len = 0;
for (int i = 0; i < 1000; i++) {
len += sprintf(s + len, "%d ", i);
}
sdssetlen(s, len); // 设置实际长度
7.3 二进制安全示例
c
// C 字符串无法处理包含 \0 的数据
char c_str[] = "abc\0def";
printf("%zu\n", strlen(c_str)); // 输出: 3(遇到 \0 就停了)
// SDS 可以处理
sds s = sdsnewlen("abc\0def", 7);
printf("%zu\n", sdslen(s)); // 输出: 7(完整长度)
八、性能优化技巧
8.1 避免频繁 realloc
c
// ❌ 低效:每次拼接都可能 realloc
sds s = sdsempty();
s = sdscat(s, "a");
s = sdscat(s, "b");
s = sdscat(s, "c");
// ✅ 高效:一次性预留空间
sds s = sdsMakeRoomFor(sdsempty(), 3);
s = sdscatlen(s, "abc", 3);
8.2 使用 SDS_NOINIT 跳过初始化
c
// 如果马上要覆盖内容,不需要清零
sds s = sdsnewlen(SDS_NOINIT, 1024);
read(fd, s, 1024); // 直接从文件读取
8.3 及时释放空闲空间
c
// 如果字符串被大幅缩短,回收多余空间
sds s = sdsnew("very long string...");
sdsclear(s); // 长度变为 0,但 alloc 还是很大
s = sdsRemoveFreeSpace(s, 0); // 释放多余空间
九、常见问题
Q1: 为什么 sds 是指向 buf 而不是头部?
答 :为了兼容 C 字符串函数。这样可以直接传给 printf("%s", s),而不需要额外转换。
Q2: sdshdr5 为什么不常用?
答:
- 不支持动态扩容(没有
alloc字段) - 长度限制太小(最大 31)
- 空字符串也会被升级为
sdshdr8
Q3: SDS 和 C++ std::string 有什么区别?
| 特性 | SDS | std::string |
|---|---|---|
| 语言 | C | C++ |
| 内存布局 | 前置头部 | 实现相关 |
| 分级存储 | 5 种类型 | 通常一种 |
| 二进制安全 | 是 | 是 |
| 小字符串优化 | 无 | SSO(Small String Optimization) |
十、总结
10.1 核心优势
✅ O(1) 获取长度
✅ 自动扩容,防止溢出
✅ 二进制安全
✅ 兼容 C 字符串
✅ 内存优化(分级存储)
10.2 设计哲学
"在保持 C 字符串兼容性的前提下,提供动态、安全、高效的字符串操作。"
10.3 适用场景
- Redis 内部的键值对存储
- 网络协议解析(RESP 协议)
- AOF/RDB 持久化
- 任何需要高性能字符串处理的场景
附录:关键宏定义速查
c
// 类型常量
SDS_TYPE_5, SDS_TYPE_8, SDS_TYPE_16, SDS_TYPE_32, SDS_TYPE_64
// 宏工具
SDS_HDR(T, s) // 获取头部指针
SDS_HDR_VAR(T, s) // 声明头部变量
sdsType(s) // 获取类型
sdslen(s) // 获取长度
sdsavail(s) // 获取可用空间
// 预分配阈值
SDS_MAX_PREALLOC = 1MB
参考源码 :Redis 8.8.2 (src/sds.h, src/sds.c)