Redis数据结构之sds基本操作函数

Posted 三石雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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--

以上是关于Redis数据结构之sds基本操作函数的主要内容,如果未能解决你的问题,请参考以下文章

Redis源码剖析 - Redis内置数据结构之字符串sds

图解Redis之数据结构篇——简单动态字符串SDS

redis设计与实现之SDS简单动态字符串

redis设计与实现之SDS简单动态字符串

redisredis 基本数据结构之String

redisredis 基本数据结构之String