Redis 5设计与源码分析
上QQ阅读APP看书,第一时间看更新

2.2 基本操作

数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。本节着重介绍创建、释放、拼接字符串的相关API,帮助大家更好地理解SDS结构。在2.2.4节列出了SDS相关API的函数名和功能介绍,有兴趣的读者可自行查阅源代码。

2.2.1 创建字符串

Redis通过sdsnewlen函数创建SDS。在函数中会根据字符串长度选择合适的类型,初始化完相应的统计值后,返回指向字符串内容的指针,根据字符串长度选择不同的类型:

    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        char type = sdsReqType(initlen); //根据字符串长度选择不同的类型
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; //SDS_TYPE_5强制转化
            为SDS_TYPE_8
        int hdrlen = sdsHdrSize(type); //计算不同头部所需的长度
        unsigned char *fp; /* 指向flags的指针 */
        sh = s_malloc(hdrlen+initlen+1); //"+1"是为了结束符’\0'
        ...
        s = (char*)sh+hdrlen; //s是指向buf的指针
        fp = ((unsigned char*)s)-1; //s是柔性数组buf的指针,-1即指向flags
        ...
        s[initlen] = '\0'; //添加末尾的结束符
        return s;
    }

注意

Redis 3.2后的SDS结构由1种增至5种,且对于sdshdr5类型,在创建空字符串时会强制转换为sdshdr8。原因可能是创建空字符串后,其内容可能会频繁更新而引发扩容,故创建时直接创建为sdshdr8。

创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。

1)创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。

2)长度计算时有“+1”操作,是为了算上结束符“\0”。

3)返回值是指向sds结构buf字段的指针。

返回值sds的类型定义如下:

    typedef char *sds;

从源码中我们可以看到,其实s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。

2.2.2 释放字符串

SDS提供了直接释放内存的方法——sdsfree,该方法通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存:

    void sdsfree(sds s) {
        if (s == NULL) return;
        s_free((char*)s-sdsHdrSize(s[-1])); //此处直接释放内存
    }

为了优化性能(减少申请内存的开销), SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。

    void sdsclear(sds s) {
        sdssetlen(s, 0); //统计值len归零
        s[0] = '\0'; //清空buf
    }

2.2.3 拼接字符串

拼接字符串操作本身不复杂,可用sdscatsds来实现,代码如下:

    sds sdscatsds(sds s, const sds t) {
        return sdscatlen(s, t, sdslen(t));
    }

sdscatsds是暴露给上层的方法,其最终调用的是sdscatlen。由于其中可能涉及SDS的扩容,sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。

    /* 将指针t的内容和指针s的内容拼接在一起,该操作是二进制安全的*/
    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;
    }

图2-5描述了sdsMakeRoomFor的实现过程。

图2-5 sdsMake RoomFor的实现过程

Redis的sds中有如下扩容策略。

1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接在柔性数组buf末尾追加即可,无须扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        void *sh, *newsh;
        size_t avail = sdsavail(s);
        size_t len, newlen;
        char type, oldtype = s[-1] & SDS_TYPE_MASK; //s[-1]即flags
        int hdrlen;
        if (avail >= addlen) return s; //无须扩容,直接返回
        ...
    }

2)若sds中剩余空闲长度avail小于或等于新增内容的长度addlen,则分情况讨论:新增后总长度len+addlen<1MB的,按新长度的2倍扩容;新增后总长度len+addlen>1MB的,按新长度加上1MB扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        ...
        newlen = (len+addlen);
        if (newlen < SDS_MAX_PREALLOC)// SDS_MAX_PREALLOC这个宏的值是1MB
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
        ...
    }

3)最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过realloc扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的buf内容移动到新位置。具体代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        ...
        type = sdsReqType(newlen);
        /* type5的结构不支持扩容,所以这里需要强制转成type8*/
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
        hdrlen = sdsHdrSize(type);
        if (oldtype==type) {
        /*无须更改类型,通过realloc扩大柔性数组即可,注意这里指向buf的指针s被更新了*/
            newsh = s_realloc(sh, hdrlen+newlen+1);
            if (newsh == NULL) return NULL;
            s = (char*)newsh+hdrlen;
        } else {
            /* 扩容后数据类型和头部长度发生了变化,此时不再进行realloc操作,而是直接重新开辟内存,
              拼接完内容后,释放旧指针*/
            newsh = s_malloc(hdrlen+newlen+1); //按新长度重新开辟内存
            if (newsh == NULL) return NULL;
            memcpy((char*)newsh+hdrlen, s, len+1); //将原buf内容移动到新位置
            s_free(sh); //释放旧指针
            s = (char*)newsh+hdrlen; //偏移sds结构的起始地址,得到字符串起始地址
            s[-1] = type; //为falgs赋值
            sdssetlen(s, len); //为len属性赋值
        }
        sdssetalloc(s, newlen); //为alloc属性赋值
        return s;
    }

2.2.4 其余API

SDS还为上层提供了许多其他API,篇幅所限,不再赘述。表2-1列出了其他常用的API,读者可自行查阅源码学习,学习时把握以下两点。

表2-1 其他常用的API

1)SDS暴露给上层的是指向柔性数组buf的指针。

2)读操作的复杂度多为O(1),直接读取成员变量;涉及修改的写操作,则可能会触发扩容。