Redis设计与实现--简单动态字符串

545

简单动态字符串

Redis构建了一种动态字符串的(simple dynamic string, SDS)的抽象类型,并将这种类型用作Redis的默认字符表示
而C语言的字符串字面量只会用作一些无需对字符串进行修改的地方,如果打印日志的地方。

SDS的定义

struct sdshdr {
    // 记录buf数组中已使用字符的数量,等于SDS保存字符串的长度
    int len;
    // 记录的是buf数组中未使用的字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

SDS遵循了C字符串以空字符结尾的惯例,遵循这一惯例可以可是使SDS直接重用一部分C字符串里面的函数。

SDS与c字符串的区别

获取字符串的长度

c字符串是不记录字符串本身的长度信息,所以如果想要获取c字符串的长度需要进行一个遍历的操作,这个操作的复杂度为O(N) 而对于SDS来说,它自身记录了SDS本身的长度,如果要获取字符串的长度信息,只需要O(1)的复杂度

缓冲区的溢出

由于C字符串中不记录自身的长度,所以当对字符串进行修改的时候,容易造成缓冲区的溢出,例如在内存中有两个相邻的字符串S1,S2,S1="Redis", S2="MongoDB"

image.png 当调用stract(S1," Cluster")操作的时候,会将S1 修改成为:Redis Cluster,但如果没有在方法的调用之前为S1分配足够的空间,那么S1的数据就会溢出到S2所在的空间,然后导致S2被修改。
而与C字符串不同的是,SDS会在修改的时候,先检查SDS的空间是否满足修改的内容,如果不满足的话,会自动的将空间扩展至所需要的大小,然后执行实际的修改操作。

修改字符串所带来的内存重分配次数

由于C字符串并不记录自身的长度,所以对于一个包含N个字符的C字符串来说,它底层存储的是一个N+1长度的字符数组(额外的一个空间是用来保存空字符串)。由于这样的一种关联性,那么当每次增加或者缩短一个C字符串的时候总是需要进行一次内存的重分配的操作。

  • 如果是增加字符的操作,那么在这个操作的之前需要通过重新分配内存来扩展底层数组空间的大小,不然就会产生缓冲的溢出
  • 如果是缩短字符串的操作,那么在这个操作之后就需要通过内存分配来释放不再使用的那部分的空间,如果忘记了就会产生内存的泄漏。 但Redis做为一个数据库,经常被使用在一些速度快,数据被频繁修改的场景里面,如果每次对于字符串的操作都需要进行一个内存的重分配的操作这是需要占用大量的时间的,且对性能造成影响。
    所以为了避免这种缺陷,基于SDS的结构,SDS通过未使用空间解除了字符串长度与底层数组空间的关系:在SDS的底层,buf数组的长度就并不一定是字符串的长度加1,数组里面可以包含没有使用的字节,而这些字节的长度就由SDS的fee属性来记录。
    基于SDS的未使用空间,SDS实现了空间预分配和惰性空间释放策略:
  • 空间预分配,这个操作简而言之就是当SDS进行修改需要扩张的时候,程序不仅会为SDS分配修改所需要的空间,同时也会为SDS分配额外的未使用空间。
    • 如果SDS进行修改之后,SDS的长度(len属性)如果小于1MB,那么程序分配和len同样大小的未使用空间,这个时候len属性将会与fee属性的值一样。
    • 如果SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。
  • 惰性空间释放,当SDS保存的字符串缩短的时候,程序并不会马上进行内存的重分配来回收缩短之后多出来的字节,而是使用fee这个属性将这些字节数量记录下来,并等待使用。

二进制安全

C字符串中字符必须符合某一种规则(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符会被误认为是字符串结尾。而这些限制使得C字符串只能保存文本内容。
但Redis需要的不仅仅是对文本内容的保存场景,有时候也需要对一些二进制数据保存的场景,为了使得Redis适用于不同的场景。SDS的所有API操作都是二进制安全的,所有SDS的API操作都会以二进制的方式来处理SDS存放在buf数组里面的数据,程序并不会对其中的数据做任何的限制,过滤和假设。数据被写入的时候是什么样的,它被读取的时候就是什么样的。

总结

  • Redis只会使用C字符串做字面量,在大多数的情况下Redis使用SDS作为字符串的表示。
  • 比起C字符串,SDS有以下的优点
    • 常数复杂度获取字符串的长度
    • 杜绝了缓冲区的溢出
    • 减少了修改字符串长度所需要的内存分配次数
    • 二进制安全
    • 兼容部分C字符串的函数

补充

Redis源码中关于SDS结构定义:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};