• Redis设计与实现读书笔记(一) SDS


      作为redis最基础的底层数据结构之一,SDS提供了许多C风格字符串所不具备的功能,为之后redis内存管理提供了许多方便。它们分别是:

    • 二进制安全
    • 减少字符串长度获取时间复杂度
    • 杜绝字符串溢出
    • 减少内存分配次数
    • 兼容部分C语言函数  

     下面将简要阐述SDS基础结构,并介绍这些功能相应的实现细节。

     SDS字符类型定义非常简单,以redis3.0.7为例:

    typedef char *sds;
    
    struct sdshdr {
        unsigned int len;    //定义当前字符串长度(不包含'\0')
        unsigned int free;   //定义当前对象可容纳的空间数
        char buf[];          
    };

      redis字符串定义的数据结构相当简单。作者从五个方面阐述了这样设计的好处:

      1.二进制安全

      C风格的字符串中的字符必须符合某种编码(ASCII),而且除了末尾以外不能保存空字符(‘\0’),而许多诸如图像,音频这样的二进制文件很有可能含有空字符。这样C风格字符串在处理某些二进制数据时可能发生数据截断,不是二进制安全。而redis作为数据库,必然要兼顾各种数据的存储,因此,通过sdshdr的len字段可以记录真实数据大小,保证二进制安全,换言之,写的数据是怎么样的,读取的时候也是怎么样的。

      2.减少字符串长度获取时间复杂度

      这个非常好理解,C风格的字符串通过遍历字符串找到‘\0’来得到字符串长度的,这样在字符串大量进行strlen操作时,会耗费许多时间。而sds在获取字符串长度时只需要读取len值即可,并且在每次对字符串进行修改时,len的变化对用户是透明的,使用一定的空间来换取速度,我认为是很合理的,许多高级语言也是这么来实现的,比如Java。

      3.杜绝字符串溢出

      使用C语言编程时,使用诸如strcat(str1,str2)字符串连接的函数,C语言假设程序员已经为str1分配了充足的空间以进行字符串的拼接。这时如果str1没有足够的空间的话,势必会造成内存泄漏。sds保证每次拼接时的操作总是正确无误的,假设空间不足,sds将启动自己的内存分配策略进行“扩容”,保证操作的正确性,避免了内存泄漏。

    /* 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));
    }

      sds通过sdscat实现拼接的,这里通过sdscatlen实现方法,我们来看看sdscatlen。

    /* 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) {
        struct sdshdr *sh;
        size_t curlen = sdslen(s);    //当前长度
    
        s = sdsMakeRoomFor(s,len);    //进行空间分配,如果空间足够是不需要进行分配的
        if (s == NULL) return NULL;   //分配失败
        sh = (void*) (s-(sizeof(struct sdshdr))); //s的结构体指针首部
        memcpy(s+curlen, t, len);                 //直接进行了字节拷贝,因为已经将需要的内存分配好了
        sh->len = curlen+len;                     //计算使用长度,长度就是当前字符串长度加上函数参数len
        sh->free = sh->free-len;                  //分配完空间后会有一个free,这时用掉了len个空间所以要减去
        s[curlen+len] = '\0';                     //s的末尾添加‘\0’
        return s;
    }

      函数的功能是将void指针指向的len个字节拼接到s上面。它首先计算了一下所需的空间和当前空间,并进行了一次空间分配,这次分配是通过sdsMakeRoomFor来实现的,我们待会会提到,分配好了以后,由于空间上一定是足够的,所以直接肆无忌惮的调用系统函数进行了拷贝,之后修改相应的属性值完成了修改。通过自定制的内存机制避免了程序员编程时遇到的内存泄漏。

      4、减少内存分配次数

      现在来看一下内存分配的sdsMakeRoomFor源码。

    /* 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) {
        struct sdshdr *sh, *newsh;
        size_t free = sdsavail(s);
        size_t len, newlen;
    
        if (free >= addlen) return s;    //空间充足
        len = sdslen(s);
        sh = (void*) (s-(sizeof(struct sdshdr)));  //新的使用空间
        newlen = (len+addlen);
        if (newlen < SDS_MAX_PREALLOC)  
            newlen *= 2;                          //分配两倍空间(1份多余空间)
        else
            newlen += SDS_MAX_PREALLOC;         
        newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);  //分配空间
        if (newsh == NULL) return NULL;
    
        newsh->free = newlen - len;                           //只修改free属性,len需要自行修改
        return newsh->buf;
    }

      函数的功能是现在有一个sds指针,需要添加addlen时的分配策略。注意这里的sds不会去改变新的len,所以需要在调用这个函数后自行修改len属性。

      从这里面可以看出sds在内存管理的策略:

      (一)空间足够直接返回了,不做任何操作。

      (二)空间不足时,如果当前空间+需要空间 < SDS_MAX_PREALLOC(1M) 那么,就分配 2*(当前空间+需要空间),如果大于1M,分配(当前空间+需要空间 + 1M),这样的话,既不会耗费过多内存,也可以减少内存分配策略,设计上很简单精妙。

      5.兼容C部分函数

      其实像java一样,直接把所有字符放在数组里,再加上len属性是不需要结束符‘\0’的,但是sds为了兼容部分C函数,还是给字符串末尾加上了空字符,毕竟Java有自己原生的System.out而C语言只有自己的printf/scanf函数用于输入输出。

      另外,为了兼容c语言程序,sds的指针其实指向的是结构体里面的char[]的,所以用到len等属性需要进行转化。

      总结:

      redis自己设计了一套迷你的字符串系统,让初学者的我茅塞顿开,这是我的第一篇博客,希望以后可以坚持写下去。

     

  • 相关阅读:
    网络协议
    工具
    GPG 导入导出 Key
    文件系统 相关
    内核常见结构体定义的位置
    sysctl 命令
    git 使用技巧
    busybox 对suid的支持
    vue开发中遇到的一些问题
    http环境下解决navigator.getUserMedia` undefined 的问题
  • 原文地址:https://www.cnblogs.com/xiaodeshan/p/5945934.html
Copyright © 2020-2023  润新知