Redis 在存储字符串时,没有使用 C 语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型。
除了用来保存数据库中的字符串值之外,SDS 还被用作缓冲区:AOF 模块的 AOF 缓冲区,以及客户端状态中的输入缓冲区。
SDS
定义
在 Redis 中 SDS 的结构如下所示:
struct sdshdr {
int len;
int free;
char buf[];
}
其中:
len
: 记录buf
数组中已使用字节的数量,即 SDS 中所保存字符串的长度;free
: 记录buf
数组中未使用字节的数量buf
: 字节数组,用于保存字符串。
SDS 遵循 C 语言字符串中以空字符结尾的惯例,保存空字符串的 1 字节不计算在 SDS 的 len 中,但会占用一个字节空间,同样也不会统计在 free 属性中。
与 C 语言的区别
1. 获取字符串长度
C 语言中的字符串并不会记录自身的长度信息,所以,为了获取长度时,需要遍历整个字符串,直接遇到结束字符为止,整个操作的时间复杂度为 O(N);
SDS 中保存了字符串长度,在获取长度时,可以直接返回,时间复杂度为 O(1)。
2. 字符串修改
在 C 语言中,字符串所分配内存等于字符串长度 + 1,在执行修改操作时,都需要对内存重新分配。
C 语言中字符串拼接时,需要手动分配足够多的内存,然后再执行拼接操作,假如内存不够,则会产生缓冲区溢出;而如果执行缩短操作时,需要执行内存重分配来释放空闲内存,否则就会生产内存泄漏;
Redis 作为数据库,且常用于对速度要求严苛、数据频繁修改的场合,如果每次修改,都需要执行一次内存分配的话,会对性能产生影响。
所以,为了解决这个缺陷,SDS 通过未使用空间(free)解除了字符串长度和底层数组长度之间的关联,在 SDS 中,buf 数组的长度 = len + free + 1。
针对字符串拼接的场景,SDS 采用了自动分配内存和空间预分配的操作。在拼接字符串时,会先检查空间是否满足要求,如果满足要求,直接使用未使用空间,当不满足要求时,会自动将 SDS 的空间拓展至所需的大小,然后再执行拼接操作,所以不需要手动分配内存;同时,程序不仅会为 SDS 分配修改所必须要的空间,还会分配额外的未使用空间,从而减少内存重分配次数。
内存分配额外空间,遵循一定的规则:
- 如果对 SDS 进行修改后,长度小于 1 MB 的话,那么长须分配和 len 属性同样大小的未使用空间,这时,len = free;
- 如果修改后,长度大于等于 1 MB ,程序会分配 1 MB 的未使用空间。
针对缩短需要释放内存的操作,Redis 并不会立即执行内存重分配来回收多出的空间,而是通过 free 属性记下,等待将来使用,应对将来字符串增长操作时,可以直接使用内存。
同时,SDS 也提供了释放空间的 API,所以不用担心内存浪费。
3. 保存内容限制
C 语言的字符串中,必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外,字符串中不能包含空字符,否则会被误认为是字符串结尾,从而限制了只能保存文本数据,而不能保存特殊的二进制数据。
而 SDS 通过 buf 数组,保存一系列二进制数据,而且,SDS 是通过 len 属性来判断字符串结尾,而不是空字符,从而避免了不能保存空字符串的限制。
根据上面,我们发现,SDS 中末尾的空字符串,并未产生任何作用,还为它分配了一个字节的内存,这是为什么呢?
其实,SDS 这样操作,是为了保证 SDS 可以重用一部分 C 语言中字符串函数,例如
<string.h>/strcasecmp
函数,使用它来比较两个字符串是否相等,SDS 就可以直接使用,避免了不必要的代码重复。
总结
Redis 使用 SDS( Simple Dynamic String ,简单动态字符串)作为字符串表示方式,其有三个属性:len
(已使用字节的长度)、free
(未使用字节的长度)、buf
(字节数组)。
相比 C 语言的字符串,SDS 具有以下优点:
- 常数复杂度获取字符串长度;
- 自动分配内存,杜绝了缓冲区溢出;
- 减少修改字符串长度时产生的内存重分配次数;
- 除了字符串数据,还可以保存二进制数据;
- 兼容部分 C 语言字符串函数。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。