Redis SDS(Simple Dynamic String)技术文档

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 五大特性

  1. 兼容 C 字符串 :以 \0 结尾,可直接传给 printf()strlen() 等标准库函数
  2. O(1) 获取长度 :头部保存 len 字段,无需遍历
  3. 自动扩容:写入前检查容量,避免缓冲区溢出
  4. 预分配策略:扩容时多分配空间,减少后续重新分配次数
  5. 二进制安全 :字符串内部可包含 \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)

相关推荐
只爱喝胡辣汤1 小时前
Redis Dict(字典/哈希表)技术文档
redis
就叫_这个吧3 小时前
RabbitMQ+elasticsearch+Redis,实现新增内容并异步到es中,是否消费成功检测
redis·elasticsearch·rabbitmq
李可以量化4 小时前
Redis Client 从了解到精通(二)下:String 类型进阶操作全解
redis·git·python·量化交易·qmt
BUG研究员_5 小时前
LangChain 向量数据库实战:Redis 与 Pinecone 的知识点总结
数据库·redis·langchain
李可以量化6 小时前
Redis Client 从了解到精通(二)上:redis-py 高级用法与核心命令实战
前端·数据库·redis·python·缓存·ptrade
仍然.6 小时前
Redis---String
数据库·redis·缓存
JavaPub-rodert6 小时前
Redis 和 MySQL 如何保证数据一致性?从缓存更新到延迟双删完整讲解
redis·mysql·缓存
半个落月7 小时前
Next.js 16 笔记应用实战:Redis 数据链路与组件两版拆分详解
前端·redis·next.js
家有娇妻张兔兔8 小时前
Redis 报错 “MISCONF Redis is configured to save RDB snapshots“?一文讲透根因与根除方案
数据库·redis·故障排查·misconf