> 文档中心 > Redis数据结构(一)SDS

Redis数据结构(一)SDS


一、关于SDS

  Redis没有直接使用C语言传统的字符串表示(也就是以空字符结尾的字符数组),而是自己构建了一种名为简单字符串的抽象类型(Simple Dynamic String),并将SDS用作了Redis默认的字符串表示。在Redis3.0之前,对于sds的结构定义如下:

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

而在3.2之后的版本结构如下所示:

/* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */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[];};

其实只是做了一下优化,为了节省空间,定义了这五种类型。

struct __attribute__ ((__packed__)) 

表示去掉对其填充,之所以去掉对其填充,是因为获取当前的SDS的类型只需要直接获取flags就可以了,若进行对齐填充,由于 Padding 的存在,我们在不同的系统中不知道退多少才能获得flags,并且我们也不能将 sds 的指针指向flags,这样就无法兼容 C 语言的函数了。

在Redis数据库中,包含字符串值的键值对子在底层都是由SDS实现的,比如执行命令

redis> SET lordky "this is test value"OK

在Redis中将会创建一个新的键值对,Key将会是一个保存着“lordky”字符串的SDS。而Value将会是一个保存着“this is test value”的SDS。
Redis数据结构(一)SDS
如图所示,SDS中free属性为0,表示没有分配任何未使用的空间,length为15表示这个SDS保存了一个15个字节长度的字符串。buf数组就是一个char类型的数组,实际保存着字符串,而最后也和C语言的字符串一样,以‘\0’结尾。这样做SDS可以直接重用一部分C语言字符串函数库里面的函数。虽然如此,但是SDS判断字符串是否结束并不是以空字符来进行判断的,正如上图所示,SDS的buf数组中间也有空字符,这是由于SDS是采用length的值来进行判断的。

二、为什么使用SDS

  1. 获取字符串长度时间复杂度为O(1)
    我们根据SDS的数据结构可以看出,要获取字符串的长度直接获取lenth值就可以了,但是针对于C语言的字符串来说,就需要遍历整个字符串数组才能获取到其长度。这样确保了获取字符串长度的工作不会称为Redis性能的瓶颈。
  2. 杜绝缓冲区溢出
    C语言的字符串还有一个问题就是容易造成缓冲区溢出。比如,在内存中,紧邻着两个C字符串s1、s2,其中s1保存着“lordky”,s2保存着"test",如图所示:
    Redis数据结构(一)SDS
    当我们执行stract(s1," king")的时候,s1的内容变成了“lordky king”,但是,如果我们在执行stract命令之前没有为s1分配足够的空间,当执行stract命令后,s1的数据将会溢出到s2中。变成如下情况:
    Redis数据结构(一)SDS
    但是SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性,当SDSAPI需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的操作,所以,SDS既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。redis3.0自动扩容的代码如下:
sds sdsMakeRoomFor(sds s, size_t addlen) {    struct sdshdr *sh, *newsh;    size_t free = sdsavail(s);    size_t len, newlen;// 当空闲空间大于新增空间的时候,直接返回,不需要扩容    if (free >= addlen) return s;    len = sdslen(s);// 获取当前长度     sh = (void*) (s-(sizeof(struct sdshdr)));    newlen = (len+addlen);// 新的长度 // #define SDS_MAX_PREALLOC (1024*1024)该定义在sds.h中    if (newlen < SDS_MAX_PREALLOC)    // 也就是当新长度小于1MB的时候直接double扩容 newlen *= 2;    else    // 当新长度大于1MB的时候直接用新长度+1MB newlen += SDS_MAX_PREALLOC;    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);    if (newsh == NULL) return NULL;    newsh->free = newlen - len;    return newsh->buf;}
  1. 减少修改字符串时带来的内存重分配次数
    对于C字符串来说,底层实现总是一个N+1个字符长的数组。每次增加或者缩短一个C语言字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作:
      如果进行曾长字符串,那么在执行操作之前,程序就需要先通过内存重分配来扩展底层数组的空间大小,如果略过这一步就会出现前面所提到的缓冲区溢出的情况
      如果程序执行的是缩短字符串的操作,那么在执行操作之后程序就需要通过内存分配来释放字符串不再使用的那部分空间,如果不进行这一步就会产生内存泄露。

    针对SDS,buf数组的长度不一定就是字符长度数量加一,数组里面可以包含未使用的字节,而这些字节的数量就有SDS的free属性记录。SDS就通过未使用空间,实现了空间预分配惰性空间释放两种优化策略

    针对空间预分配:
    空间预分配用于优化SDS字符串增长的操作,也就是SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配所必须要的空间,还会为SDS分配额外的未使用的空间。具体逻辑正如上面自动扩容代码中所述一样,当新长度小于1MB的时候直接double扩容,当新长度大于1MB的时候直接用新长度+1MB

    惰性空间释放:
    惰性空间释放用于优化SDS的字符串缩短的操作,当SDS需要缩短保存的字符串的时候,程序不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的熟练记录下来,等待将来使用。这样就避免的重复对一个SDS进行操作所产生的内存分配开销。当然,SDS也提供了相应真正释放不用内存空间的方法sdsRemoveFreeSpace以避免内存泄露的问题

  2. 二进制安全
      针对于C语言的字符串,除了字符串末尾之外,字符串里面不能包含空字符串,否则最先被程序读入的空字符将会被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存类似图片、音频、视频等二进制数据。
      但是对于SDS来说,正如我们开始提到的,SDS判断字符串是否结束并不是以空字符来进行判断的,而是采用SDS的len属性来进行判断的。而Redis为了可以适应各种不同的使用场景(因为很多时候需要保存二进制数据),SDS的API都是二进制安全的。针对buf字节数组,Redis不是用这个数组来保存字符,而是用它来保存一系列的二进制数据。

风车动漫