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所示:

Redis源码分析01——简单动态字符串(sds)_第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技术研究)