Skip to content

简单动态字符串

简单动态字符串(simple dynamic string, SDS),是由 Redis 构建的一种抽象类型。Redis 中默认字符串表示就是使用简单动态字符串

当需要一个无须进行修改的字面量时,就会使用 C 语言中的传统字符串。如打印日志:

shell
redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");

当需要一个可以被修改的字符串值时, Redis 就会使用 SDS 来表示此字符串值。如 Redis 数据库中,包含字符串的键值对就是由 SDS 实现。

shell
redis> SET msg "hello world"
OK

SDS 的定义

SDS 结构如下:

c
struct sdshdr {
    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

下面用一个 SDS 示例来帮助大家更好的理解其结构:

SDS 示例

  • free 的值为 0,表示这个 SDS 没有任何未使用空间
  • len 的值为 5,表示这个 SDS 保存了一个 5 字节长的字符串
  • buf 是一个 char 类型的数组,数组的前 5 个字节分别保存了 hellos,最后一个字节保存了空字符 \0

SDS 遵循 C 语言字符串以空字符结尾的惯例,保存空字符的 1 字节的空间不计算在 SDSlen 属性中。而额外分配 1 字节空间、添加空字符到字符串末尾等操作,都是由 SDS 函数自动完成,所以这个空字符对使用者来讲是完全透明的。遵循空字符结尾的好处是 SDS 可以直接重用一部分 C 语言字符串的函数库里面的函数,如:使用 <stdio.h> 中的 printf 函数进行打印,

c
printf("%s", s->buf);

SDS 与 C 语言字符串的区别

获取字符串长度的复杂度为常数级别

因为 SDS 中的 len 属性记录了本身的长度,所以获取长度的复杂度为 O(1)。而 C 语言字符串获取长度的复杂度是 O(n),因为只有遍历到结尾的空字符时,才能知道长度。

不存在缓存溢出的问题

SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当需要对 SDS 进行修改时,相关 API 会先检查 SDS 空间是否满足修改所需的需求,如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以 SDS 不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出问题。简单来说,空间不满足,自动扩容。

减少修改字符串时引发的内存重分配次数

SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联:在 SDS 中,buf 数组的长度不一定就是字符数量加 1,数组里面可能包含未使用的字节,这些字节的数量由 SDSfree 属性记录。

通过未使用空间,SDS 实现了空间预分配惰性空间释放两种优化策略。

空间预分配

用于优化 SDS 的增长操作。当需要对 SDS 进行空间扩展时,除了分配修改所必需的空间,还会分配额外的空间。额外分配的空间由以下公式决定:

  • 如果对 SDS 操作后,SDS 的长度(len 的值)小于 1M,那么程序分配和 len 同样大小的未使用空间。
  • 如果对 SDS 操作后,SDS 的长度(len 的值)大于等于 1M,那么程序会分配 1M 的未使用空间。

通过空间预分配策略,SDS 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降为最多 N 次。

惰性空间释放

用于优化 SDS 的字符串缩短操作:当 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。同时,SDS 也提供了 API,让我们在有需要时,真正释放 SDS 未使用的空间,所以不用担心会造成内存浪费。

通过惰性空间释放策略,避免了缩短字符串时的内存重分配操作,并为将来可能有的增长操作提供了优化。

二进制安全

C 语言字符串中的字符必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外,字符串里不能包含空的字符,否则最先被程序读入的空字符会被认为是字符串的末尾,这些限制使得 C 字符串只能保存文本数据,而不能保存 像图片、音频、视频、压缩文件这样的二进制数据。所有的 SDS 操作都是二进制安全的,都会以处理二进制的方式来处理存放在 buf 数组里的数据,程序不会对其中的数据做任何限制、过滤或者假设,数据在写入时什么样,那么在读取时就是什么样。

兼容部分 C 语言字符串函数

通过遵循 C 语言字符串以空字符为结尾的惯例,SDS 可以在有需要时重用 <string.h> 函数库,从而避免了代码重复。

本节回顾

Redis 只会在字面量使用 C 语言字符串。其他情况下,Redis 使用 SDS 作为字符串表示。

比起 C 语言字符串,SDS 具有以下优点:

  1. 获取字符串长度的复杂度为常数级别
  2. 不存在缓存溢出的问题
  3. 减少修改字符串时引发的内存重分配次数
  4. 二进制安全
  5. 兼容部分 C 语言字符串函数