Redis系列之SDS

Redis没有直接使用C语言的字符串,而是自己构建了一个名为简单动态字符串(simple synamic string)的抽象类型,并将SDS作为Redis默认的字符串表示。当Redis存储的不仅仅是一个字符串面量,而逝一个可以被修改的字符串的时候,就会用到SDS。例如:使用SET msg "hello world"存储,底层是通过SDS实现的。

一、SDS的定义

SDS结构体:

struct sdshdr {
    int len;
    int free;
    char buf[];
}

上边的结构体是SDS的定义,包含三个属性信息,len表示的是字符串的长度,在存储的时候直接计算好的,free表示SDS除了存储的字符串之外,额外预留的空间,buf[]用于存储字符串的值,需要注意的是:在存储的时候除了存储字符串本身的值之外,还会在值的结尾存储一个空字符'\O',这一点遵循了C字符串以空字符结尾的惯例,这么做的好处是,SDS可以直接重用C字符串函数库中的函数。

SDS与C字符串的区别

(一)、查询长度的复杂度

C字符串使用长度为N+1的字符数组来表示长度为N的字符串,在字符串的结尾使用一个空字符结束。因为C语言在存储字符串的时候并不直接计算字符串的长度,所以获取字符串长度需要使用遍历来实现,时间复杂度为O(n);SDS在存储字符串值的时候直接计算并写入len,要获取字符串长度直接查询len即可,实现复杂度为O(1),Redis通过使用SDS,将查询字符串的长度的时间复杂度从O(n)降低到了O(1)。

(二)、杜绝缓冲区溢出

由于C字符串在存储的时候不计算字符串长度,所以strcat假设用户执行这个函数时已经为dest分配了足够多的内存空间,可以存储所有的字符串值,但是如果strcat的字符串长度大于分配的缓存空间,就会造成一部分字符串存进去了,另一部分由于没有空间存储不了,造成缓冲区的溢出、数据不安全。
在Redis中使用SDS,SDS结构中有一个属性free,该属性为SDS存储字符串后又额外预留了一部分存储的空间。在需要会已存入的字符串追加值的时候会先检查free的大小,如果free大于要追加的字符串长度,则直接执行追加操作。反之,API会先将SDS的free修改至追加值的长度后,再执行追加操作。扩展free的操作不需要手动,Redis会自动完成。

(三)、减少修改字符串时的内存重分配次数

在C字符串操作的时候,如果是拼接操作,需要先执行内存重分配来扩展底层数组空间的大小,然后执行拼接操作,如果忘记了内存充分配会造成内存溢出。如果执行截断操作,需要在执行截断后,执行内存重分配来释放掉不在使用的内存空间,如果忘记了后一步操作就会造成内存泄漏。
在Redis中,SDS通过未使用空间解除了字符串长度和底层数组长度的关联:buf的长度不一定是存储的字符串长度加1,数组里包含着未使用的字节,这些字节的数量由free来记录。SDS解决C字符串问题提供了如下两个策略:

  • 空间预分配
    空间预分配用于优化字符串增长的操作,当SDS的API需要对一个SDS修改,并且需要进行空间扩展的时候,程序不仅会为SDS分配需要使用的空间,还会为SDS分配额外的未使用的空间。如果修改之后SDS的长度len小于1M,程序会为SDS分配与len属性等值的额外空间;如果SDS修改之后长度len大于1M,程序会为SDS分配1M的额外内存空间。 通过空间预分配策略,Redis减少了连续执行字符串增长带来的内存重分配次数,将C的N次降低到最多N次。
  • 惰性空间释放
    惰性空间释放用于优化字符串的缩短操作:当SDS的API需要缩短保存的字符串时,在执行缩短操作之后并不会直接释放因为缩短操作带来的多余空间,而是将这些空间长度记录到free中,留待将来使用,这样既减少了内存重分配的次数,也为将来有可能增长的操作提供了优化。SDS还提供了API,在有需要的时候真正的释放SDS未使用的空间,这样就不会造成内存的浪费。

(四)、二进制安全

C字符串中的字符必须符合某种编码,并且除了字符串末尾的空字符之外,字符串里不能包含空字符,否则会被程序误读为结尾,这些限制造成C字符串只能保存文本数据,而不能保存图片、音频、视频等二进制数据。
在Redis中为了适应各种存储的场景,SDS的API都是二进制安全的(binary-safe),所有需要存储的数据都会被API以二进制的方式处理后在存储到buf中,不会对存储的数据类型做限制,所以是二进制安全的。

你可能感兴趣的:(redis)