Redis 简单动态字符串 SDS

简单动态字符串

Redis 是用 c 语言编写的,但是 Redis 没有用 c 语言传统的字符串表示(以空字符串结尾的字符数组),而是自己构建了简单动态字符串SDS (simple dynamic string) 的类型。

Redis 默认用 SDS 表示字符串。

当 Redis 需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会用 SDS 表示字符串,在Redis 中 包含字符串的键值对底层都是用SDS实现的。

SDS 的定义

struct sdshdr {

    // buf 已占用长度
    int len;
    // buf 剩余可用长度
    int free;
    // 实际保存字符串数据的地方
    char buf[];
};
  • len表示字符串的长度。
  • free表示buf中剩余可用长度
  • buf是存储字符串数组的地方

SDS 遵循 c 语言以空格结尾的惯例,这么做的好处是SDS 可以重用一部分 c 语言字符串函数库里面的函数。结尾的空格不计在len属性中。

下图展示了一个存入 Hello 的SDS结构:

SDS 与 c 字符串

c 语言字符串使用N+1长度的字符数组存储长度为N的字符串,并且字符串最后一位是 '\0'。如下图:

下面讨论Redis为什么使用SDS来表示默认的字符串,以及两个的区别。

常数复杂度获取字符串长度

因为 c 语言字符串不记录自身长度信息,所以要获取字符串长度需要遍历整个字符产,直到遇到 '0\' 停止。时间复杂度为O(N)

但是对于 SDS 来说,自身记录字符串长度信息,所以获取字符串长度时间复杂度为O(1)

所以 Redis 使用SDS,使获取字符串的长度操作只需O(N)复杂度,大大的提高了系统的性能。 

杜绝缓冲区溢出

c 语言中使用 <string.h>/strcat 函数拼接字符串。函数定义如下:

char *strcat(char *dest, const char *src);

strcat 函数可以把 src 内容拼接到 dest 末尾,strcat 函数假定执行时,dest 后面又足够的空间容纳 src 中的字符串,而这个假定不成立时就有可能造成缓冲区溢出。

如下图例子,假设程序内存中有紧邻着的字符串s1和s2,其中s1为 Hello s2为 World:

 

 这时,程序执行一下代码:

strcat(s1, "Golang")

s1的内容改为 "HelloGolang",但是没有给s1分配足够的空间,导致s1的内容溢出到s2中,导致s2的内容被修改:

 

 而SDS的空间分配策略,杜绝了缓冲区溢出。当SDS API 需要修改SDS时,API会先检测SDS的空间是否满足修改要求,如果不满足,API会自动将SDS空间扩容至合适的大小,然后才执行修改操作。

而这个扩容也是有策略的,下一节将提到。

修改字符串时减少内存分配次数

对于 c 语言来说,每次修改字符串都需要从新分配内存

  • 增长字符串

    比如 append 操作,程序需要先分配内存来扩容底层数组空间,如果没有,会导致缓冲区溢出。

  • 缩短字符串

    比如 trim 截断操作,那么程序需要释放截断部分的内存,如果没有,会导致内存泄漏。

由于 Redis 是数据库,会经常有数据修改的场合,如果每次修改都要重新分配内存,释放内存,会对性能造成影响

为了避免 c 字符串这种缺陷,SDS 实现了以下策略:

空间预分配

空间预分配用于优化SDS的字符产增长操作,当SDS API 对SDS 字符串进行增长操作,并且需要对SDS空间进行扩容时,程序不仅会分配所需的空间,还会为SDS分配所谓空间额外空间。

其中额外分配空间由一下规则决定:

  • 修改SDS后,SDS长度(len属性)小于1MB

    那么额外分配的空间大小等于分配的大小,即 len == free

  • 修改SDS后,SDS长度(len属性)大于1MB

    那么额外分配的空间大小等于1MB.

通过这样的策略,可以减少空间分配次数,把改N次优化为最多改N次。

惰性空间释放

惰性空间释放用于优化SDS的字符串截断操作,当SDS API 对 SDS字符串进行截断操作时,SDS并不会释放空间,而是用free属性存储起释放空间的大小,等待将来使用。

下一次扩容时直接使用这部分空间,减少内存重新分配次数。

二进制安全

c 语言中,字符串必须符合特定的编码,并且除了尾部的空格外,中间不能包含空格,否则遇到空格程序会以为到了尾部。这使得 c 字符串只能存文本文件。

但是SDS是由len属性判断是否字符串是否结束,所以SDS可以存二进制文件

部分兼容 c 字符串函数

c 字符串要求字符数组的末尾必须是\0,作为字符串尾的标记。而SDS中的字符数组也遵循了这一规范,所以仍然可以使用C字符串相关函数,因此避免了重复代码。

参考文献

redis 设计与实现(第二版)

https://www.jianshu.com/p/1b814c3173f1

posted @ 2020-10-26 10:49  hulunbao  阅读(200)  评论(0编辑  收藏  举报