• 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 at the

     * end of the string. However the string is binary safe and can contain

     * 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] = ''; // 与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] = ‘’)。

    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] = '';

    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--

  • 相关阅读:
    SqlServer注意事项总结,高级程序员必背!
    C#语法——委托,架构的血液
    C#语法——泛型的多种应用
    C#——Nhibernate探索
    C#语法——await与async的正确打开方式
    谈谈我理解的SA——Systems Architecture
    C#线程安全使用(五)
    C#语法——元组类型
    架构师的御人之道
    另一个角度的架构师
  • 原文地址:https://www.cnblogs.com/exceptioneye/p/6921751.html
Copyright © 2020-2023  润新知