在 redis 中,字符串的基本结构如下,其中, len 表示字符串的长度, alloc 表示字符串的最大容量, flags 表示header的类型。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已占用buf长度 */
uint8_t alloc; /* 申请的buf长度 */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
buf 表示需要存储的字符数组,数组的长度为len+1(由于需要存储一个结束符’/0’)。
具体结构如下,
除了上面提到的 sdshdr8 ,还包含 sdshdr5、sdshdr16、sdshdr32、sdshdr64 。
在读取字符串时,首先需要获取当字符串的存储类型,
// 类型 - flags取值 #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4 // 用来获取字符串内容 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
其中, SDS_HDR 和 SDS_HDR_VAR 可以从sds字符串中获取header的起始位置。
sds包含很多功能,
sds sdsnewlen(const void *init, size_t initlen); sds sdsnew(const char *init); sds sdsempty(void); sds sdsdup(const sds s); void sdsfree(sds s); sds sdsgrowzero(sds s, size_t len); sds sdscatlen(sds s, const void *t, size_t len); ...
这里只详细介绍下初始化和追加函数,
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen); // 通过初始化长度获取数据类型
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1); // 申请内存空间:header长度+buf长度+1
if (!init)
memset(sh, 0, hdrlen+initlen+1); // 全部设置为0
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen; // 获取buf指针位置
fp = ((unsigned char*)s)-1; // 获取类型flag字段
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
...
}
if (initlen && init)
memcpy(s, init, initlen); // 拷贝数据
s[initlen] = '/0'; // buf最后一位置为'/0'
return s;
}
为sds增加可用空间,
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);
if (newlen < SDS_MAX_PREALLOC) // 2倍的新长度
newlen *= 2;
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); // 如果类型发生改变,则重新申请新类型数据,并拷贝buf数据
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
string获取字符串长度的时间复杂度为O(N),需要遍历整个字符串;而sds不再需要遍历字符串,通过len字段可以直接获取存储在buf内字符串的长度,时间复杂度为O(1)。
对于C语言来说,每一次字符串长度的增加,都会造成一次内存分配的操作;每一次字符串长度的减少,都会造成一次内存释放操作。如果redis同样需要平凡的内存分配和释放,对性能会造成严重的影响。
sds通过alloc字段来记录预分配空间的大小,len字段来记录当前存储字符串的长度。当有资源需要释放时,sds只是减少len的大小;当需要增加空间时,只有当剩余的空位不足,才会重新申请新的空间,否则只需要增加len的大小。