简单动态字符串

简单动态字符串

redis的字符串表示不是使用的像C语言那种以空字符结尾的字符数组,而是自己构建了一种简单动态字符串(simple dynamic string)——SDS

而一般来说,redis只会在字符串字面量被用在一些无需对字符串进行更改的地方使用C风格的字符串,比如打印日志。

SDS的定义

sds定义在sds.h的sdshdr的结构体中:

1
2
3
4
5
struct sdshds {
int len; //记录buf数组中已经使用的字节数
int free; //buf数组中未使用的字节数
char buf[];//与C字符串一样以空字符结尾的字符数组
}

之所以在SDS中,使用了与C字符串类似的风格,是因为这样可以直接使用C字符串函数库里面的函数,比如要打印字符串,直接使用:

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

SDS与C字符串的区别

要了解两者之间的区别,首先要从结构体入手。

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

这个比较好理解,因为结构体本身带有长度域,而设置和更新SDS长度的工作又是由SDS的API执行时自动完成的,所以避免了影响redis的性能。

对于C字符串来说,要获取字符串长度,只能逐个遍历,直到遇到了空字符。

杜绝缓冲区溢出

这个也比较好理解。对于一些字符串拼接的操作,如果没有记录字符串长度,无法得知字符串剩余的内存,这样在拼接的时候就可能发生缓冲区溢出。

而SDS的API在使用sdscat函数进行字符串拼接的时候,会先检查SDS的空间是否足够,不足的话,会使用某种分配策略扩展空间。

减少修改字符串带来内存重分配次数

对于C字符串来说,每次增长或者缩短一个字符串,程序都需要对保存该字符串的数组进行一次重新分配内存的工作,一旦没有重新分配,就可能造成内存泄露或者产生缓冲区溢出。而且因为内存重分配涉及到了复杂的算法,并且有可能会执行系统调用,所以通常是比较耗时的操作。

为了避免C字符串的这种缺陷,redis的SDS实现了空间预分配和惰性空间释放两种优化策略。

  1. 空间预分配

这个策略主要用于优化SDS的字符串增长操作,在进行空间扩展时,不仅仅会分配所需空间,还会为SDS分配额外的未使用空间;

  • 如果对SDS修改后,长度小于1MB,那么程序将分配和len属性同样大小的未使用空间。即如果SDS的len变成了13字节,那么整个buf的数组长度将会变成13+13+1=27个字节;
  • 如果对SDS修改后,长度大于1MB,那么程序将会分配1MB的未使用空间;
  1. 惰性空间释放

该策略会使得SDS在缩短长度时,不会马上使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节数记录下来,并等待将来使用

二进制安全

相比C字符串在读到空字符,则认为是字符串结尾,redis的SDS则是用len属性来检验字符串是否结束。这种二进制安全的做法使得redis不仅可以保存文本格式,还可以保存任意格式的二进制数据