简介
在Redis
中STRING类型数据结构使用了简单动态字符串(simpple dynamic string, SDS)去实现。Redis
没有直接使用C语言中的字符串,而是采用SDS去保存字符串的值,主要原因有一下几点。
- C语言中获取一个字符串长度时间复杂度为O(n),而在SDS中这一操作时间复杂度仅为O(1)
- 在C语言中容易出现缓冲区溢出的问题,由于没有记录字符串长度,在合并两个字符串时可能出现空间不足而使得合并后字符串的内容被覆盖
- SDS将连续增⻓N次字符串所需的内存分配次数从必定 N次降低为最多N次
- C语言只能保存文本数据,而图片、视频、音频等二进制数据无法保存
下面通过分析SDS原理去解释以上几点原因
SDS结构
Redis
源码中sds.h
头文件定义了SDS
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS结构如上图所示,与C语言一样,使用空字符作为字符串的结尾,而保存空字符的1字节空间不计算在len属性里面,遵循空字符结尾这一惯例的好处是SDS可以直接使用C语言字符串库函数里的一部分函数
获取字符串长度
在C语言中获取一个字符串长度必须从头开始遍历,直到遇到空字符结尾,这一操作时间复杂度为O(n)
而Redis
只需要访问SDS中len的值就可以得到字符串的长度,时间复杂度仅为O(1)
Redis
通过使用SDS将获取字符串长度的时间复杂度从O(n)降到了O(1)
解决缓冲区溢出的问题
C语言不记录字符串长度造成的另一个问题是容易出现缓冲区溢出的情况,例如,使用strcat
函数可以将一个字符串添加到另一个字符串的末尾,char *strcat(char *dest, const char *src)
但是C语言没有记录字符串的长度,strcat
函数认为dest的内存足够用,一旦假设不成立就会出现缓冲区溢出,如下图所示。
由于没有给s1分配足够的空间,strcat
函数执行后字符串s1的数据溢出到s2的空间,导致之前s2保存的数据被覆盖掉。SDS的空间分配策略解决了该问题,当SDS API对SDS进行修改时,API会先检查SDS的空间是否
满足修改的需求,不满足的话API会将SDS的空间扩展,因而使用SDS不需要手动修改SDS的空间大小,这样就避免了出现缓冲区溢出的情况。
空间分配策略
C字符串在执行增长字符串操作之前需要通过内存重新分配来扩展底层数组的空间大小,否则可能会产生缓冲区溢出。如果是执行缩短字符串操作,那么程序需要通过内存重分配来释放不再使用的那部分空间,否则会导致内存泄漏。内存重分配涉及复杂的算法,以及需要使用系统调用,因此是比较耗时的,而对于Redis
这样的数据库来说,往往需要频繁修改字符串,这样显然会影响性能。Redis
通过SDS未使用空间解除了字符串长度和底层数组之间的关联,SDS字符数组长度不一定是字符串长度加一,数组里面可以包含未使用的空间,这些未使用的字节空间由free属性记录。利用未使用空间,Redis
实现了空间预分配和惰性空间释放两种策略。
空间预分配
SDS采用空间预分配策略优化了字符串的增长操作,当SDS API需要对字符串修改时,程序不仅会为SDS分配足够的空间,而且还会分配额外的未使用空间。分配额外的未使用空间数量由以下规则确定。
- 如果对SDS修改之后,SDS的长度(即len属性的值)小于1MB,那么程序会分配和len属性同样大小的未使用空间,这时SDS len属性的值和free属性的值一样
- 如果对SDS修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间
通过空间预分配策略,SDS将连续增长N次字符串所需的内存重新分配次数从必定N次降低为最多N次
惰性空间释放
惰性空间释放用于优化SDS字符串缩短操作,当SDS API需要将字符串缩短时,程序不会马上使用内存重分配把剩余空间进行回收,而是把未使用的字节记录到free属性,等待将来使用。通过这种策略,SDS避免了字符串缩短时内存重分配操作。
二进制安全
C字符串中的字符必须符合某些编码格式,并且除了字符串末尾,字符串里面不能包含空字符,否则最先输入的空字符被认为是字符串的结尾,这样导致C字符串只能保存文本数据,而不能保存像图片、视频等二进制数据。在Redis
中,所有SDS API都是二进制安全的,SDS使用len属性的值来判断字符串是否结束,而不是空字符。
参考资料
《Redis设计与实现》