redis-sds源码阅读

sds结构

struct sdshdr {

    /** buf 中已占用空间的长度*/
    int len;

    /** buf 中剩余可用空间的长度*/
    int free;

    /** 数据空间*/ 
    /** 柔性数组*/
    char buf[];
};

Alt text

获取字符串长度复杂度为O(1)

在sds的结构中,保存了sds的占用空间的长度,可以使用sdslen进行读取,代码实现如下:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

二进制安全

二进制安全:比如说在c语言中,strlen计算长度,是依赖于特殊的字符'\0'来判断是否到达字符串的最末尾,所以对于"aaa\0bbb"来说,它就不是二进制安全的
sds的二进制安全,它不仅仅根据"\0"来判断是否到达字符串的末尾,还根据len的长度来进行计算

杜绝缓冲区溢出

缓冲区溢出,指的是在修改字符串的时候,没有对该字符串分配足够的空间
sds在对字符串进行修改的时候,都会使用(sdsMakeRoomFor)判断是否有足够的空间
sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;
    // 原有字符串长度
    size_t curlen = sdslen(s);

    // 扩展 sds 空间
    // T = O(N)
    s = sdsMakeRoomFor(s, len);

    // 内存不足?直接返回
    if (s == NULL) {
        return NULL;
    }

    // 复制 t 中的内容到字符串后部
    // T = O(N)
    sh = (void*) (s - (sizeof(struct sdshdr)));
    memcpy(s + curlen, t, len);

    // 更新属性
    sh->len = curlen + len;
    sh->free = sh->free - len;

    // 添加新结尾符号
    s[curlen + len] = '\0';

    // 返回新 sds
    return s;
}

动态扩展,空间预分配

对字符串进行修改的时候,会判断sds的剩余空间,如果大小不足够会重新进行分配,具体分配逻辑如下:
    1、如果新长度小于SDS_MAX_PREALLOC定义的最大空间,就分配两倍所需空间;
    2、否则在目前的长度上再增加SDS_MAX_PREALLOC定义的最大空间

代码实现如下:
/*
 * 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
 * buf 至少会有 addlen + 1 长度的空余空间
 * (额外的 1 字节是为 \0 准备的)
 *
 * 返回值
 *  sds :扩展成功返回扩展后的 sds
 *        扩展失败返回 NULL
 *
 * 复杂度
 *  T = O(N)
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;

    // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);

    size_t len, newlen;

    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) {
        return s;
    }

    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*)(s - (sizeof(struct sdshdr)));

    // s 最少需要的长度
    newlen = (len + addlen);

    // 根据新长度,为 s 分配新空间所需的大小
    // 如果当前占用内存小于1M那么就分配两倍于当前长度的内存
    // 如果当前占用内存大于等于1M那么就分配当前长度+1M的内存
    // 内存增长策略*****
    if (newlen < SDS_MAX_PREALLOC) {
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    } else {
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    }

    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + 1);

    // 内存不足,分配失败,返回
    if (newsh == NULL) { 
        return NULL;
    }

    // 更新 sds 的空余长度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

惰性释放,减少内存分配次数

对sds字符串进行缩减操作的时候,不会对空间进行释放,仅仅只是改变len属性和free属性的值

void sdsclear(sds s) {

    // 取出 sdshdr
    struct sdshdr *sh = (void*) (s - (sizeof(struct sdshdr)));

    // 重新计算属性
    sh->free += sh->len;
    sh->len = 0;

    // 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
    sh->buf[0] = '\0';
}

long数值转字符串

这个函数比较有意思,他将long long类型的数值转换成string类型
    1、将数值按照10取模,取模后的值保存,在除10(相当于是对当前位的上一位进行获取 12345第一次取模的值是5,除10变成1234.5取模的值变成4)(注意101.1与10取模是1不是1.1)
    2、将保存的字符串再进行翻转
    比如:
        12345 取模后得到的值依次是5,4,3,2,1 连接成最后的字符串是54321
        再进行倒转以后变成12345

int sdsll2str(char *s, long long value) {
    char *p, aux;
    unsigned long long v;
    size_t l;

    /* Generate the string representation, this method produces
     * an reversed string. */
    v = (value < 0) ? -value : value;
    p = s;
    do {
        *p++ = '0' + (v%10);
        v /= 10;
    } while(v);
    if (value < 0) *p++ = '-';

    /* Compute length and add null term. */
    l = p - s;
    *p = '\0';

    /* Reverse the string. */
    p--;
    while(s < p) {
        aux = *s;
        *s = *p;
        *p = aux;
        s++;
        p--;
    }
    return l;
}

为什么要'0'+单个数字?
参考ascii表,字符串1对应的ascii码是49,'0'对应的ancii值是48,比如说数字3想存储到字符串中表示也为'3',那么它对应的ascii就是3+48也就是3+'0'
坚持原创技术分享,您的支持将鼓励我继续创作!