redis字符串源码解析(sds.h/sds.c)

redis字符串源码解析(sds.h/sds.c)

字符串的定义

首先看一下redis字符串结构体

/*
 * 类型别名,用于指向 sdshdr 的 buf 属性
 */
typedef char *sds;

struct sdshdr {   
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间 实际上不占用内存空间sizeof(struct sdshdr) = 8
    char buf[];
};

这个都很好理解,使用结构体struct sdshdr来保存字符串;

注意:struct sdshdr中的char buf[],是针对可变长结构体的编程技巧,必须强调这点,否则后面没法理解。sizeof(struct sdshdr) === 8(柔性数组成员不占用结构体的空间,只作为一个符号地址存在,而且必须是结构体的最后一个成员。柔性数组成员不仅可以用于字符数组,还可以是元素为其它类型的数组。C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括**柔性数组的内存。包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。)

新建redis字符串

/*
 * 根据给定的初始化字符串 init 和字符串长度 initlen
 * 创建一个新的 sds
 *
 * 参数
 *  init :初始化字符串指针
 *  initlen :初始化字符串的长度
 *
 * 返回值
 *  sds :创建成功返回 sdshdr 相对应的 sds
 *        创建失败返回 NULL
 *
 * 复杂度
 *  T = O(N)
 */
sds sdsnewlen(const void *init, size_t initlen) {

    struct sdshdr *sh;

    // 根据是否有初始化内容,选择适当的内存分配方式
    // T = O(N)
    if (init) {
        // zmalloc 不初始化所分配的内存
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        // zcalloc 将分配的内存全部初始化为 0
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }

    // 内存分配失败,返回
    if (sh == NULL) return NULL;

    // 设置初始化长度
    sh->len = initlen;
    // 新 sds 不预留任何空间
    sh->free = 0;
    // 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
    // T = O(N)
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    // 以 \0 结尾
    sh->buf[initlen] = '\0';

    // 返回 buf 部分,而不是整个 sdshdr
    return (char*)sh->buf;
}

这个函数调用例如:sdsnewlen(“redis”,5)

会根据init判断内存是否初始化,所以有两种初始化方式,分配内存大小都是

sizeof(struct sdshdr)+initlen+1,1是为了给\0;

注意: return (char)sh->buf;*

返回的是buf的指针,而不是sh,如果后面我们想改变结构体中的len和free怎么办呢。

这里有内存的知识需要注意,我们的结构体在内存中存放是这样的:

|5|0|“redis”|现在返回的指针指向"redis",当我们使用sh->buf的内存减去两个int就能得到sh的指针。所以就有后面的

得到一个字符串len和free大小

/*
 * 返回 sds 实际保存的字符串的长度
 *
 * T = O(1)
 */
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

/*
 * 返回 sds 可用空间的长度
 *
 * T = O(1)
 */
static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}

这里真是利用了内存的关系,通过sh->buf====>sh

free扩容函数

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 分配新空间所需的大小
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字符串的free部分扩容的。仅仅增加free的长度,不会改变sds字符串的长度。 这个函数比较难理解的地方是:扩容的幅度这里,为什么扩新长度的2倍

sdscatlen 将长度为 len 的字符串 t 追加到 sds 的字符串末尾

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

1.首先使用扩容函数sdsMakeRoomFor扩容,注意,每次使用扩容函数后,需要判断返回值是否为空

2.然后使用memcpy函数从s+curlen处开始拷贝。

3.最后一定要记住改变字符串的状态(free和len)。

sdsll2str 将long long类型转化为字符串

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. */
    //将ll类型的数值转换为ull
    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. */
    // 开始反转字符串,让个位数到最后,这里采用交换的方式,两个指针s和p分别向中间靠,用一个中介aux指针做交换
    p--;
    while(s < p) {
        aux = *s;
        *s = *p;
        *p = aux;
        s++;
        p--;
    }
    return l;
}

源码还提供了sdsull2str函数,将ull转换为str,方法一样;

这两个函数是为了给输入用的,redis提供了其它接口调用它

// 根据输入的 long long 值 value ,创建一个 SDS
sds sdsfromlonglong(long long value) {
    char buf[SDS_LLSTR_SIZE];
    \\
    int len = sdsll2str(buf,value);

    return sdsnewlen(buf,len);
}

sdscatvprintf格式化字符串

字符串格式化是非常有用的工具,它可以让我们通过指定格式生成一个字符串。SDS字符串库也提供了相应的方法:

为了编写可变参数函数,我们通常需要用到头文件,可参考博文

https://www.cnblogs.com/ThatsMyTiger/p/6924462.html 然后再看这段源码,会简单很多。

\\看过printf源码的对这个函数应该很熟悉
\\va_list变参输入
sds sdscatprintf(sds s, const char *fmt, ...) {
    va_list ap;
    char *t;
    va_start(ap, fmt);
    // T = O(N^2)
    t = sdscatvprintf(s,fmt,ap);
    va_end(ap);
    return t;
}

打印任意数量个字符串,并将这些字符串追加到给定 sds 的末尾

sds sdscatvprintf(sds s, const char *fmt, va_list ap) {
    va_list cpy;
    char staticbuf[1024], *buf = staticbuf, *t;
    size_t buflen = strlen(fmt)*2;

    /* We try to start using a static buffer for speed.
     * If not possible we revert to heap allocation. */
     /*
    这里先用栈区的staticbuf,如果fmt的2倍长度超过这个staticbuf在从堆去分配
    这样做如果是小于1024的,直接用栈区的内存,非常快
    这是预分配冗余空间的惯用手段,减小对内存的频繁分配
    */
    if (buflen > sizeof(staticbuf)) {
        buf = zmalloc(buflen);
        if (buf == NULL) return NULL;
    } else {
        buflen = sizeof(staticbuf);
    }

    /* Try with buffers two times bigger every time we fail to
     * fit the string in the current buffer size. */
    while(1) {
        // 设置倒数第二个字符为结束字符,方便后面判断是否buf占满
        buf[buflen-2] = '\0';
        va_copy(cpy,ap);
        // T = O(N)
        
        // vsnprintf函数可以参考此链接https://blog.csdn.net/qq_37824129/article/details/78763286
        vsnprintf(buf, buflen, fmt, cpy);
        // 如果buf占满,释放buf,将buf*2
        if (buf[buflen-2] != '\0') {
            if (buf != staticbuf) zfree(buf);
            buflen *= 2;
            buf = zmalloc(buflen);
            if (buf == NULL) return NULL;
            continue;
        }
        break;
    }

    /* Finally concat the obtained string to the SDS string and return it. */
    t = sdscat(s, buf);//这里底层调用的sdscatlen,是安全的
    //如果一次存完,调用栈内存,操作系统会帮忙回收,但是如果调用了堆内存,则需要手动释放
    if (buf != staticbuf) zfree(buf);
    return t;
}

这段代码厉害在,对内存使用的细节和va使用,可以好好研读

上面分析的关键函数是redis实现SDS的核心函数,像外部接口sdsnew底层调用的是sdsnewlen,sdscpy、sdscat等底层调用的是sdscatlen。

其实c语言的字符串已经能够满足基本全部需求,为什么redis还要自己实现字符串sds呢?
要回答这个问题,还是回到开头说的c语言对于字符串的一般定义。其通常如下:

  1. char *buf1 = “redis_5.0”;
  2. char buf2[] = “redis_5.0”;
    这两种都表示一个字符串常量,第一种方式不可以在修改,第二种方式可以修改,但是大小固定。再想想平时对字符串的操作函数,strcpy、strcat等函数,一般是不安全的(二进制安全)。

那么我们对比redis的实现,可以看出redis具有以下优点:

  1. 兼容c语言字符串
  2. 对于普通字符串的操作是安全的
  3. 可以动态扩展空间(最大是512M)
  4. 对字符串求长度的复杂度为O(1)
  5. 底层用的是数组,操作很快
  6. 从sdsMakeRoomFor的实现,我们知道redis采用了预分配冗余空间的方式来减小内存的频繁分配

你可能感兴趣的:(设计模式,C++)