Redis源码解析01: 简单动态字符串SDS

Redis没有直接使用C字符串(以’’结尾的字符数组),而是构建了一种名为简单动态字符串( simple  dynamic  string, SDS)的抽象类型,SDS设计API实现对字符串的各种修改。

1:SDS的定义

  在sds.h中,定义了结构体sdshdr表示SDS,其定义如下:

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

  len记录SDS保存的字符串的长度(不包括末尾的'');free记录buf中未使用的字节数量(也不包括’‘);buf是字节数组,用于保存字符串。比如下面的例子:  

  

  结合上图,很好理解free,len,buf字段的意义

  sds也提供了查询sds实例free,len的接口,这些接口可以在O(1)复杂度的情况下查询字符串的长度、未使用空间。

static inline size_t sdslen(const sds s)   
{  
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));  
    return sh->len;  
}  
  
static inline size_t sdsavail(const sds s)   
{  
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));  
    return sh->free;  
}  

2:SDS与C字符串的区别

        C字符串不记录自身的长度信息,获取一个C字符串的长度的时间复杂度为O(N)。SDS在len属性中直接记录了字符串的长度,所以获取一个SDS字符串长度的事件复杂度是O(1)。这确保获取字符串长度的工作不会成为Redis的性能瓶颈。即使对一个非常长的字符串键反复执行”strlen”命令,也不会对系统性能造成任何影响。

        C字符串不记录长度带来的另一个问题是容易造成缓冲区滋出。比如strcat函数将src字符串中的内容拼接到dest字符串的末尾, 如果dst的长度不足以容纳src,就会产生缓冲区滥出。

  与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS的API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。

sds sdscatlen(sds s, const void *t, size_t len)   
{  
    struct sdshdr *sh;  
    size_t curlen = sdslen(s);  
  
    s = sdsMakeRoomFor(s,len);  
    if (s == NULL) return NULL;  
    sh = (void*) (s-(sizeof(struct sdshdr)));  
    memcpy(s+curlen, t, len);  
    sh->len = curlen+len;  
    sh->free = sh->free-len;  
    s[curlen+len] = '';  
    return s;  
}  
  
sds sdscat(sds s, const char *t)   
{  
    return sdscatlen(s, t, strlen(t));  
}  

  sdscat是通过sdscatlen实现的,在sdscatlen中,首先用sdsMakeRoomFor保证SDS具有足够的空间(sdsMakeRoomFor的函数实现见下面),然后才是将字符串t追加到s中。其他所有修改SDS的API都会通过sdsMakeRoomFor保证缓冲区不会溢出。

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);  
    if (newlen < SDS_MAX_PREALLOC)  
        newlen *= 2;  
    else  
        newlen += SDS_MAX_PREALLOC;  
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);  
    if (newsh == NULL) return NULL;  
  
    newsh->free = newlen - len;  
    return newsh->buf;  
}  

  其中,SDS_MAX_PREALLOC的值就是1024*1024,也就是1M。参数addlen表示需要扩容的长度。

  可以发现对SDS空间拓展的时候分两种情况:  

  如果对SDS进行修改之后,SDS的长度小于1MB,那么程序将分配和len属性同样大小的未使用空间,这时SDS的 len属性的值将和free属性的值相同。

        如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。比如,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1 MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。

  通过在修改SDS之前调用sdsMakeRoomFor函数,确保不会出现溢出的问题。

  

原文地址:https://www.cnblogs.com/lovelaker007/p/8676101.html