Redis数据结构之sds基本操作函数
本文及后续文章,Redis版本均是v3.2.8
本篇文章讲解sds基本操作函数,我们从源码角度来进一步理解。
一、sds创建函数和销毁
sds创建函数
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
*
* 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 sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
// 空的字符串通常被创建成type 8,因为type 5已经不实用了。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 得到sds的header的大小
int hdrlen = sdsHdrSize(type);
unsigned char *fp; // flags字段的指针
// s_malloc等同于zmalloc,+1代表字符串结束符
sh = s_malloc(hdrlen+initlen+1);
if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
// s为数据部分的起始指针
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1; // 得到flags的指针
// 根据字符串类型来设定header中的字段
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;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen); // 拷贝数据部分
s[initlen] = '\0'; // 与C字符串兼容
return s; // 返回创建的sds字符串指针
}
sdsnewlen创建一个长度为initlen的sds字符串,并使用init指向的字符数组(任意二进制数据)来初始化数据。如果init为NULL,那么使用全0来初始化数据。它的实现中,我们需要注意的是:
-
如果要创建一个长度为0的空字符串,那么不使用SDS_TYPE_5类型的header,而是转而使用SDS_TYPE_8类型的header。这是因为创建的空字符串一般接下来的操作很可能是追加数据,但SDS_TYPE_5类型的sds字符串不适合追加数据(会引发内存重新分配)。
-
需要的内存空间一次性进行分配,其中包含三部分:header、数据、最后的多余字节(hdrlen+initlen+1)。
-
初始化的sds字符串数据最后会追加一个NULL结束符(s[initlen] = ‘\0’)。
sds释放函数
我们知道,sds的释放采用zfree来释放内存。
其实现代码如下:
/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
if (s == NULL) return;
// 得到内存的真正其实位置,然后释放内存
s_free((char*)s-sdsHdrSize(s[-1]));
}
对于sdsfree,我们需要注意的是:
内存要整体释放,所以要先计算出header起始指针,把它传给s_free函数。这个指针也正是在sdsnewlen中调用s_malloc返回的那个地址。
二、sds动态调整和sds空余空间回收
sds动态调整函数
sds最重要的性能就是动态调整,Redis提供了扩展sds容量的函数。
/* Enlarge the free space at the end of the sds string so that the caller
* is sure that after calling this function can overwrite up to addlen
* bytes after the end of the string, plus one more byte for nul term.
*
* Note: this does not change the *length* of the sds string as returned
* by sdslen(), but only the free buffer space we have. */
// 在原有的字符串中取得更大的空间,并返回扩展空间后的字符串
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); // 获取sds的剩余空间
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 如果剩余空间足够,则直接返回
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
// sds规定:如果扩展后的字符串总长度小于1M则新字符串长度为扩展后的两倍
// 如果大于1M,则新的总长度为扩展后的总长度加上1M
// 这样做的目的是减少Redis内存分配的次数,同时尽量节省空间
if (newlen < SDS_MAX_PREALLOC) // SDS_MAX_PREALLOC = 1024*1024
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 根据sds的长度来调整类型
type = sdsReqType(newlen);
// 不使用SDS_TYPE_5,一律按SDS_TYPE_8处理
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 获取新类型的头长度
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
// 如果与原类型相同,直接调用realloc函数扩充内存
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
// 如果类型调整了,header的大小就需要调整
// 这时就需要移动buf[]部分,所以不能使用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
s[-1] = type; // 设定新的flags参数
sdssetlen(s, len); // 更新len
}
sdssetalloc(s, newlen); // 更新sds的容量
return s;
}
对于sdsMakeRoomFor实现代码,我们需要注意的是:
-
如果原来字符串中的空余空间够用(avail >= addlen),那么它什么也不做,直接返回。
-
如果需要分配空间,它会比实际请求的要多分配一些,以防备接下来继续追加。它在字符串已经比较长的情况下要至少多分配SDS_MAX_PREALLOC个字节,这个常量在sds.h中定义为(1024*1024)=1MB。
-
按分配后的空间大小,可能需要更换header类型(原来header的alloc字段太短,表达不了增加后的容量)。
-
如果需要更换header,那么整个字符串空间(包括header)都需要重新分配(s_malloc),并拷贝原来的数据到新的位置。
-
如果不需要更换header(原来的header够用),那么调用一个比较特殊的s_realloc,试图在原来的地址上重新分配空间。s_realloc的具体实现得看Redis编译的时候选用了哪个allocator(在Linux上默认使用jemalloc)。但不管是哪个realloc的实现,它所表达的含义基本是相同的:它尽量在原来分配好的地址位置重新分配,如果原来的地址位置有足够的空余空间完成重新分配,那么它返回的新地址与传入的旧地址相同;否则,它分配新的地址块,并进行数据搬迁。
sds空余空间回收
/* Reallocate the sds string so that it has no free space at the end. The
* contained string remains not altered, but next concatenation operations
* will require a reallocation.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
// 用来回收sds空余空间,压缩内存,函数调用后,s会无效
// 实际上,就是重新分配一块内存,将原有数据拷贝到新内存上,并释放原有空间
// 新内存的大小比原来小了alloc-len大小
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t len = sdslen(s); // 获取字符串的实际大小
sh = (char*)s-sdsHdrSize(oldtype);
type = sdsReqType(len);
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+len+1); // 申请的内存大小为hdrlen+len,原有的空余空间不算
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
newsh = s_malloc(hdrlen+len+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, len);
return s;
}
三、sds连接操作函数
sds提供了字符串的连接(追加)函数,用来连接两个字符串。
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
* end of the specified sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
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;
}
/* Append the specified null termianted C string to the sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
/* Append the specified sds 't' to the existing sds 's'.
*
* After the call, the modified sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
sdscatlen将t指向的长度为len的任意二进制数据追加到sds字符串s的后面。比如append操作使用sds的sdscatlen来实现。
在sdscatlen的实现中,我们看到先调用sdsMakeRoomFor来保证字符串s有足够的空间来追加长度为len的数据。sdsMakeRoomFor可能会分配新的内存,也可能不会。
我们从sdscatlen的函数接口,可以看到一种使用模式:调用它的时候,传入一个旧的sds变量,然后它返回一个新的sds变量。由于它的内部实现可能会造成地址变化,因此调用者在调用完之后,原来旧的变量就失效了,而都应该用新返回的变量来替换。不仅仅是sdscatlen函数,sds中的其它函数(比如sdscpy、sdstrim、sdsjoin等),还有Redis中其它一些能自动扩展内存的数据结构(如ziplist),也都是同样的使用模式。
四、sds其他操作函数
sds sdsempty(void); // 清空sds
sds sdsdup(const sds s); // 复制字符串
sds sdsgrowzero(sds s, size_t len); // 扩展字符串到指定长度
sds sdscpylen(sds s, const char *t, size_t len); // 字符串的复制
sds sdscpy(sds s, const char *t); // 字符串的复制
sds sdscatfmt(sds s, char const *fmt, ...); //字符串格式化输出
sds sdstrim(sds s, const char *cset); //字符串缩减
void sdsrange(sds s, int start, int end); //字符串截取函数
void sdsupdatelen(sds s); //更新字符串最新的长度
void sdsclear(sds s); //字符串清空操作
void sdstolower(sds s); //sds字符转小写表示
void sdstoupper(sds s); //sds字符统一转大写
sds sdsjoin(char **argv, int argc, char *sep); //以分隔符连接字符串子数组构成新的字符串
五、总结
sds是Redis中最基本的数据结构,使用一整段连续的内存来存储sds头信息和数据信息。其中,字符串的header包括了sds的字符串长度,字符串的最大容量以及sds的类型这三大信息。这样做的好处有很多,能让很多操作的复杂度降低,比如获取sds中字符串长度的操作,只需要O(1)即可,比strlen的O(N)好很多。
--EOF--
![](https://images.cnblogs.com/cnblogs_com/exceptioneye/978972/t_qrcode_for_gh_3c9b028059b0_430.jpg)
作者:三石雨
出处:http://www.cnblogs.com/exceptioneye
再烦,也别忘微笑;再急,也要注意语气。
再苦,也别忘坚持;再累,也要爱自己。
低调做人,你会一次比一次稳健;高调做事,你会一次比一次优秀。