Redis源码分析01——简单动态字符串(sds)

Posted 彼 方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码分析01——简单动态字符串(sds)相关的知识,希望对你有一定的参考价值。

本文对应Redis源代码的 src/sds.csrc/sds.h

Redis为了更有效率地管理字符串的内存,自己构建了一种简单动态字符串(simple dynamic string,简称sds),并将sds作为Redis的默认字符串来使用。sds会根据针对不同的长度的数据采用不同的数据结构,以达到节省内存的目的。如下共五种,其中SDS_TYPE_5并不使用,平时只是直接访问其标志字节:

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

这里以SDS_TYPE_8类型为例讲解:

typedef char *sds;

/*
__attribute__ ((packed)) 的作用就是告诉编译器取消结构体在编译过程的优化对齐,按照实际占用字节数进行对齐
*/

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用的数据长度 */
    uint8_t alloc; /* 去掉头和'\\0'结束符,有效长度+数据长度 */
    unsigned char flags; /* 低三位表示类型, 其余五位未使用,小端格式 */
    char buf[]; /* 可变长结构 */
};

内存示例如图1-1所示:

图1-1

Redis为了支持sds的使用了一系列的函数,接下来就来看一下几个重要函数的具体实现:

  1. 新内存分配(_sdsnewlen
    • 如果init的值为NULL,则将缓冲区全部初始化为0
    • 使用init的值为SDS_NOINIT,则不对缓冲区进行初始化
    • 生成的sds字符串总是以’\\0’结尾,这样可以兼容C类型字符串
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    // 获取initlen长度对应的类型
    char type = sdsReqType(initlen);
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp;
    size_t usable;

    assert(initlen + hdrlen + 1 > initlen);
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    if (sh == NULL) return NULL;

	// 不初始化缓冲区
    if (init==SDS_NOINIT)
        init = NULL;
	// 将缓冲区全部置零
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);

	// s指向的是buf,即可变长结构
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    switch(type) {
		// SDS_TYPE_5没有只有一个flags和buf,所以其长度是存储在flags的高五位的,这点和其它几个是不一样的
        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 = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
	// 如果符合条件,则将缓冲区初始化为init的内容
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\\0';
    return s;
}
  1. 内存扩容(sdsMakeRoomFor
    • 当前剩余有效长度>=新增长度,直接返回
    • 新增后的内存长度如果小于预分配长度(1024*1024)的话,则扩容一倍;其余情况每次增加预分配长度(1024*1024)
    • 判断新旧类型是否一致,如果一致则直接使用realloc,否则使用malloc分配一块新内存,然后free掉旧内存
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;
    int hdrlen;
    size_t usable;

    // 当前剩余有效长度>=新增长度,直接返回
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    assert(newlen > len);

	// 新增后的内存长度小于预分配长度(1024*1024),扩容一倍,SDS_MAX_PREALLOC=1024*1024
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
	// 其余情况每次增加预分配长度(1024*1024)
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > len);
	
	// 新旧类型一致则直接使用remalloc
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
	// 新旧类型不一致则使用malloc分配一块新内存,然后free掉旧内存
    } else {
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        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);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}
  1. 内存缩容(sdsRemoveFreeSpace
    • 当前剩余有效长度为0(空间已用完),直接返回
    • 根据当前数据的长度获取缩容之后的类型
    • 判断新旧类型是否一致,如果一致则直接使用realloc进行缩容,否则使用malloc分配一块新的更小的内存,然后free掉旧内存
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    size_t avail = sdsavail(s);
    sh = (char*)s-oldhdrlen;

    // 当前剩余有效长度为0(空间已用完),直接返回
    if (avail == 0) return s;

    // 获取当前数据长度对应的SDS类型,后续需拿这个进行和当前实际类型进行比较
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

	// 新旧类型一致则直接使用remalloc进行缩容
    if (oldtype==type || type > SDS_TYPE_8) {
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
	// 新旧类型不一致则使用malloc分配一块新的更小内存,然后free掉旧内存
    } 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优点如下:

  • 可以避免缓冲区溢出(由len和alloc配合实现)
  • 减少字符串修改带来的内存频繁重分配次数
  • 二进制操作安全,可以保持文本数据,也可以保持任意格式的二进制数据
  • 以’\\0’结尾,兼容C类型字符串
  • sds是char*的别名,可以理解为分配的是一块连续内存(头部字段+数据),根据局部性原理可以提高访问速度
  • 利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的

表1-1给出Redis为sds实现的一些API,当然,因为sds兼容C类型字符串,所以大多数情况下C的那些字符串函数也是可以正常使用的

函数参数作用
sdslen(const sds s)获取给定sds的长度
sdsavail(const sds s)获取给定sds的剩余可使用长度
sdssetlen(sds s, size_t newlen)将给定sds的长度设置为newlen的值
sdsinclen(sds s, size_t inc)将给定sds的长度增加inc
sdsalloc(const sds s)获取给定sds的总容量
sdssetalloc(sds s, size_t newlen)将给定sds的总容量设置为newlen的值
sdsnewlen(const void *init, size_t initlen)调用前面讲到的_sdsnewlen,创建一个新的sds对象
sdstrynewlen(const void *init, size_t initlen)调用前面讲到的_sdsnewlen,创建一个新的sds对象
sdsnew(const char *init)创建一个包含给定C类型字符串的sds
sdsempty-创建一个不包含任何内容的空sds
sdsdup(const sds s)创建一个给定sds的副本(深拷贝)
sdsfree(sds s)释放给定的sds
sdsgrowzero(sds s, size_t len)用空字符将给定sds扩展至长度为len
sdscatlen(sds s, const void *t, size_t len)将指定sds扩容到长度为len,然后将t的前len个字符到拼接到指定sds的末尾
sdscat(sds s, const char *t)将给定C类型字符串拼接到sds的末尾(底层使用的是sdscatlen函数)
sdscatsds(sds s, const sds t)将给定sds拼接到另一个sds的末尾(底层使用的是sdscatlen函数)
sdscpylen(sds s, const char *t, size_t len)将指定sds扩容到长度为len,然后从t中复制前len个字符到指定的sds中
sdscpy(sds s, const char *t)将给定C类型字符串复制到sds中(底层使用的是sdscpylen函数)
sdscatvprintf(sds s, const char *fmt, va_list ap)按指定格式将字符串拼接到指定sds的末尾(与printf系列函数类似)
sdscatprintf(sds s, const char *fmt, …)按指定格式将字符串拼接到指定sds的末尾(与printf系列函数类似)
sdscatfmt(sds s, char const *fmt, …)按指定格式将字符串拼接到指定sds的末尾(Redis自己实现的,相比printf系列函数少了很多功能)
sdstrim(sds s, const char *cset)接受一个sds和一个C类型字符串作为参数,从sds左右两端分别移除所有在C类型字符串中出现过的字符
sdsrange(sds s, ssize_t start, ssize_t end)保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除
sdsupdatelen(sds s)更新指定sds的长度
sdsclear(sds s)清空sds保存的字符串内容
sdscmp(const sds s1, const sds s2)对比两个sds是否相同
sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count)使用指定的分割符将字符串s给分割各个子串,子串是sds格式的
sdsfreesplitres(sds *tokens, int count)将sdssplitlen生成的结果释放掉
sdstolower(sds s)将指定sds转换为小写
sdstoupper(sds s)将指定sds转换为大写
sdsfromlonglong(long long value)将value(整型)转换为sds
sdscatrepr(sds s, const char *p, size_t len)将转义字符串的表示形式拼接到指定sds的末尾
sdsmapchars(sds s, const char *from, const char *to, size_t setlen)修改字符串,将from字符串中指定的字符集的所有匹配项替换为to字符串中的相应字符
sdsjoin(char **argv, int argc, char *sep)使用指定的分隔符sep连接C类型字符串数组,返回结果为sds类型
sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen)同sdsjoin,唯一不同的是sdsjoinsds可以指定分割符的长度,而sdsjoin的分割符必须是C类型字符串
sdstemplate(const char *template, sdstemplate_callback_t cb_func, void *cb_arg)执行模板字符串的展开,并将结果作为新分配的sds返回。可以使用花括号指定模板变量,例如{variable}。如果要打印的是花括号,可以使用{{或}}来实现,类似于printf系列函数打印%的方法
sdsMakeRoomFor(sds s, size_t addlen)内存扩容,前面有详细介绍
sdsIncrLen(sds s, ssize_t incr)将给定sds的长度增加incr,并将增加完后的末尾处置’\\0’
sdsRemoveFreeSpace(sds s)内存缩容,前面有详细介绍
sdsAllocSize(sds s)获取指定sds的总占用空间大小(包括头部字段和末尾的’\\0’)
sdsAllocPtr(sds s)获取指定sds的头指针(指向头部字段开始处的指针)
sds_malloc(size_t size)分配内存
sds_realloc(void *ptr, size_t size)扩展或收缩内存
sds_free(void *ptr)释放内存
表1-1 sds API

以上是关于Redis源码分析01——简单动态字符串(sds)的主要内容,如果未能解决你的问题,请参考以下文章

Redis源码分析01——简单动态字符串(sds)

Redis源码分析01——简单动态字符串(sds)

Redis源码解析01: 简单动态字符串SDS

redis源码分析——3简单动态字符串

Redis源码剖析--动态字符串SDS

redis源码学习simple dynamic strings(简单动态字符串 sds)