Redis没有直接使用C字符串(以’\\0’结尾的字符数组),而是构建了一种名为简单动态字符串( simple dynamic string, SDS)的抽象类型,SDS设计API实现对字符串的各种修改。
1:SDS的定义
在sds.h中,定义了结构体sdshdr表示SDS,其定义如下:
struct sdshdr { unsigned int len; unsigned int free; char buf[]; };
len记录SDS保存的字符串的长度(不包括末尾的‘\\0‘);free记录buf中未使用的字节数量(也不包括’\\0‘);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] = ‘\\0‘; 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函数,确保不会出现溢出的问题。