Redis的SDS和C语言自带字符串有什么不同?
首先介绍一下SDS,它是Redis中的动态字符串,是Redis字符串的一种编码方式。其定义为
相较于C语言中的字符串多了两个属性,len和free。
这样做有什么好处呢?
1. 常数时间复杂度获取字符串长度
获取字符串的长度的时间复杂度为O(1)。C 语言中字符串获取自身长度需要进行遍历,因此时间复杂度为O(n);而SDS获取长度只需要读取len
属性,时间复杂度为O(1)。
2. 避免缓冲区溢出
除了获取字符串长度的复杂度高之外, C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。
例如,当我们使用C语言中的stract()
函数时,一定要保证为dest
分配足够多的内存,可以容纳src
字符串中的所有内容,如若没有分配,就会造成缓冲区溢出。
char *strcat(char *dest, const char *src);
假设程序中有两个在内存中紧邻着的字符串s1
和s2
,其中s1保存了字符串“Redis”
,s2保存了字符串“MogoDB”
,如图所示:
而此时执行
strcat(s1, " Cluster");
但在执行上述语句前没有给s1
分配足够的空间,那么在stract
函数执行后,s1
的数据将溢出到s2
所在的空间,导致s2
保存的内容被意外地修改。
而 SDS 在执行拼接操作之前会先检查字符串的长度是否足够,当发现字符串目前的空间不足以拼接时,会先将字符串空间扩展,然后进行修改,从而避免了缓冲区溢出。
3. 减少修改字符串的内存重新分配次数
正如前两个小节所说, 因为 C 字符串并不记录自身的长度, 所以对于一个包含了 N
个字符的 C 字符串来说, 这个 C 字符串的底层实现总是一个 N+1
个字符长的数组(额外的一个字符空间用于保存空字符'\0'
)。
因为 C 字符串的长度和底层数组的长度之间存在着这种关联性, 所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作:
- 如果程序执行的是增长字符串的操作, 比如拼接操作(append), 那么在执行这个操作之前, 程序需要先通过内存重分配来扩展底层数组的空间大小 —— 如果忘了这一步就会产生缓冲区溢出。
- 如果程序执行的是缩短字符串的操作, 比如截断操作(trim), 那么在执行这个操作之后, 程序需要通过内存重分配来释放字符串不再使用的那部分空间 —— 如果忘了这一步就会产生内存泄漏。
而 SDS 由于存在 len
属性和 free
属性,对于修改 SDS 采用了空间预分配和惰性空间释放两种策略:
- 空间预分配 对 SDS 字符串进行扩展时,扩展的内存比实际需要的多,这样可以避免在执行字符串连续增长操作所需的内存分配次数;
- 惰性空间释放 对字符串进行缩短操作时, 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用
free
属性将这些字节的数量记录起来, 并等待将来使用。
4. 二进制安全
C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。而所有的 SDS 的 API 都是以处理二进制的方式来处理 buf
里面的元素,并且 SDS 不用空字符判断字符串结束,而用 len
属性判断。
5. 兼容部分 C 字符串函数
虽然 SDS 的 API 都是二进制安全的, 但它们一样遵循 C 字符串以空字符结尾的惯例: 这些 API 总会将 SDS 保存的数据的末尾设置为空字符, 并且总会在为 buf
数组分配空间时多分配一个字节来容纳这个空字符, 这是为了让那些保存文本数据的 SDS 可以重用一部分 <string.h>
库定义的函数。