在Redis中String类型的底层数据结构名为(simple dynamic string SDS)简单动态字符串,我们知道Redis是用C语言编写的,但是Redis并没有直接使用C语言中的字符串,而是自己新创建了一种,让我们一起学习一下吧!
SDS结构
/* 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[];
};
len:记录buf数组中已使用的字节的长度。
alloc:已分配的空间,不包含头和空终止符。
flags:前三位表示不同的sds结构体的类型,后五位预留。
buf:字节数组,用于保存字符串。
以’\0’表示字符串结尾
为了兼容并使用C字符串函数库中的函数,SDS保持了C字符串的规则,以’\0’表示字符串结尾。
同时由于C字符串是以’\0’表示字符串结尾,所以C字符串只能保持文本数据,而不能保存例如图片、视频等二进制数据,因为如果字符串中出现’\0’会被误认为是字符串的结尾标识, 但是在SDS结构中却能保证二进制安全,因为SDS保存了len属性,这就可以不适用’\0’这个标识来判断字符串是否结束。
The string is always null-termined (all the sds strings are, always) so
even if you create an sds string with:
mystring = sdsnewlen("abc",3);
You can print the string with printf() as there is an implicit \0 at the
end of the string. However the string is binary safe and can contain
\0 characters in the middle, as the length is stored in the sds header.
使用SDS的好处
1、获取字符串长度的复杂度降低
因为在sds的头中包含了len属性,所以在获取一个字符串长度时它的复杂度是O(1),而C字符串则必须遍历直到遇到’\0’,所以它的复杂度是O(n)。
2、避免内存溢出
在C字符串中为一个老的字符串追加字符,如果预先没有分配好足够的内存空间,则会导致数据溢出的问题,而使用sds则不会,因为sds提供的api会在空间不足的情况下,自动进行扩容操作。
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
//确保了空间足够足够拼接新的字符
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
3、空间预分配
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
//如果空间足够,则直接返回
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
//#define SDS_MAX_PREALLOC (1024*1024)
//如果拼接后的字符串长度小于1M,则为新的字符串分配原长度2倍的空间
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
//如果超过1M,则多分配1M的空间
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
//如果类型没改变
if (oldtype==type) {
//重新分配已经分配好的内存
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
//动态分配一个新的内存空间
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
这就说明了,每次字符串的扩容,并不是扩充到刚好为当前字符串的长度,而是根据当前字符串的长度,要么扩充到原来的2倍,要么扩充1M。
这样做的目的是为了解决C字符串每次扩容都需要进行内存分配的问题,为了减少内存分配的次数,而预先分配一定的空间,使得原来扩充N次需要进行N次内存分配,优化到了扩充N次最多进行N次内存分配。
4、惰性删除
清空字符串操作,会将sds字符串长度修改为0,但是缓冲区并不会被回收,只是修改为可用空间,这样之后如果又要对字符串做拼接操作的话就不需要再重新分配内存空间。
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
总结