Redis源码阅读【1-简单动态字符串】

Redis源码阅读【1-简单动态字符串】
Redis源码阅读【2-跳跃表】
Redis源码阅读【3-Redis编译与GDB调试】
Redis源码阅读【4-压缩列表】
Redis源码阅读【5-字典】
Redis源码阅读【6-整数集合】
Redis源码阅读【7-quicklist】
Redis源码阅读【8-命令处理生命周期-1】
Redis源码阅读【8-命令处理生命周期-2】
Redis源码阅读【8-命令处理生命周期-3】
Redis源码阅读【8-命令处理生命周期-4】
Redis源码阅读【番外篇-Redis的多线程】
建议搭配源码阅读源码地址

1、介绍

简单动态字符串(Simple Dynamic Strings SDS)是Redis的基本数据结构之一,主要用于存储字符串和整型数据。SDS兼容C语音标准字符串处理函数,并且在此保证了二进制安全

二进制安全主要是针对类似于 \0 等有特殊含义的转义字符保证其安全性,而且不损害其内容

2、SDS 基本结构

首先我们看看SDS在C语言中的基本结构体是怎么样的

struct sds {
	int len;  // buf 已经占用的字节长度
	int alloc; // 总长度 (不包括头和空终止符)
	char buf[]; // 数据空间
}
//这里的成员属性不一定就是使用 int 也可能是更大或者更小的数据类型

SDS基本结构如下图所示(属性长度不一定就是4):
SDS基本结构
之所以使用这种方式来存放字符串,是因为SDS结构体中的地址是连续的,这样能通过偏移量的方式快速查找内存内容,同时也能通过buf的地址非常快速获取结构体SDS的首地址。

3、SDS 类型

从上面的图片中看出,SDS 占用的空间,除了本身buf 实际数据占用的空间,还有 len alloc 等结构属性也会占用一定的空间大小,但是如果Redis中存储了大量的短字符,那么这种结构体的头部无疑是对空间的浪费,而Redis本身就是主打性能和空间,这样的空间浪费,是不能容忍的,于是Redis 对 SDS 做出了不同的划分,分别如下:

//小于一字节
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; 
    char buf[];
};

//一字节
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;
    uint8_t alloc; 
    unsigned char flags; 
    char buf[];
};

//2字节
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; 
    uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};

//4字节
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; 
    uint32_t alloc; 
    unsigned char flags; 
    char buf[];
};

//8字节
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; 
    uint64_t alloc; 
    unsigned char flags; 
    char buf[];
};
//注意这样都是使用uint 这样能最大的使用空间

如上述代码中展示的那样,在结构体中引入了一个 flags 的单字节标记来区分SDS的类型

在单字节模式下 flags 的空间使用如下图所示:
flage结构
其中,低3位用来区分当前SDS的类型,高5位用来存储当前SDS的数据长度,所以 SDS5 的范围也只有【0~31】,flags 后面也就是实际的数据内容了,除此之外其它的 SDS 类型的头结构基本上就是 len ,alloc ,flags 所以头部空间基本上就是 S[len + alloc + flags],其中 len 和 alloc 根据不同类型的SDS使用不同的大小,以保证节约空间的目的,同时 flags 与 SDS5 一样,只有前三位存储类型,而后五位不存储数据

 __attribute__ ((__packed__))

需要关注这块,结构体会按照其所有变量结构体做最小公倍数字节对齐。当使用 packed 修饰后,结构体会按照 1字节对齐。以 SDS32 为例 ,修饰前按照 12(4x3)字节对齐,修饰后按照1字节对齐。
修饰前后内存空间如下图所示。
结构体对齐
这样做有一下几个好处:

1、节约内存:如SDS32可以节省3个字节
2、buf指针引用:SDS返回给上层的,不是结构体首地址,而是 buf 指针地址,这样可以通过 buf[-1] 直接获得 flags ,来识别当前 sds 结构体的类型,从而获取整个结构体的任意一个部分

4、 创建字符串

Redis 通过 sdsnewlen 函数创建 SDS。函数会根据字符串长度来选择合适的SDS 类型,待数据填入完成后,会返回 SDS buf 的指针作为 SDS 的指针。
如下代码:

/**
 * 入参有两个,一个是初始化字符串的指针,另一个是当前字符串的字节长度
 */
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    //根据长度选择合适的sds类型
    char type = sdsReqType(initlen);
    //如果本身是空字符,那么直接使用SDS8 而不是 SDS5 因为 SDS5 不适合空字符
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    //计算当前类型SDS 头部字节大小
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags 指针. */
    // 按照 头部空间 + 字符串大小 + 1 分配空间 (+1是为了结束符号 \0)
    sh = s_malloc(hdrlen+initlen+1);
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen; // s 是指向 buf 的指针
    fp = ((unsigned char*)s)-1; //s 是buf的指针 -1 即指向 flags
    //按照类型初始化 SDS
    switch(type) {
        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 = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    //添加末尾结束字符
    s[initlen] = '\0';
    return s;
}

5、释放字符串

SDS提供了直接释放内存的方法-sdsfree,该方法通过对 sds 指针的偏移,可以定位到 sds 的首部,然后调用 s_free释放内存:

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1])); //这里的s_free 就是 free
}

除此之外 sds 还提供了 sdsclear 方法去清空字符串,目的是为了优化性能,不直接释放内存,而是将sds的len设置为0,新的数据可以在此之上覆盖,从而不必再重新分配内存。

void sdsclear(sds s) {
    sdssetlen(s, 0); //设置 len 为0
    s[0] = '\0';  // buf 直接设置为结束字符
}

sdsclear 和 sdsfree 的差别是 sdsfree 会直接调用 free 是直接释放内存的使用权,而 sdsclear只是清空,允许后续相近的字符串能在此之上进行使用,场景并不是很通用,但是性能上比 sdsfree 要好

6、拼接字符串

拼接字符是通过sdscatsds来实现的

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

sdscatsds 是封装给上层使用的,sdscatlen才是具体的实现。调用sdscatlen可能会发生扩容的场景,其中调用sdsMakeRoomFor去检查字符串是否需要扩容,若无需扩容则直接返回,需要扩容会返回扩容后的 sds。
代码如下:

/**
 * 两个入参,原sds  和 需要增加的空间大小
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s); // 查找当前 sds 剩余可用空间的大小
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    //有足够的空间不需要扩容
    if (avail >= addlen) return s;
    
    len = sdslen(s); //获取当前sds 的长度
   
    sh = (char *) s - sdsHdrSize(oldtype); //定位到 sds 的头部 【buf地址 - 当前header长度】
    
    newlen = (len + addlen);  //获得目标需要的总共大小空间
    
    if (newlen < SDS_MAX_PREALLOC) //新长度大于 1mb 的按照 2倍扩容  SDS_MAX_PREALLOC 是最小分配大小
        newlen *= 2;
    else //新长度小于 1mb 的 按照 1mb 扩容 SDS_MAX_PREALLOC 是最小分配大小
        newlen += SDS_MAX_PREALLOC;
    type = sdsReqType(newlen); //按照新大小确定需要分配的sds类型
   
    //强制把 sds5 变成 sds8  因为 sds 5 是无法得知剩余空间的 不支持扩容
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
   
    hdrlen = sdsHdrSize(type);//根据新的sds类型确定 头部长度
   
    //判断新旧类型是否一样
    if (oldtype == type) {
        newsh = s_realloc(sh, hdrlen + newlen + 1); //追加分配分配sds 的 buf 空间  realloc 扩大空间
        if (newsh == NULL) return NULL;
        s = (char *) newsh + hdrlen; //定位到新sds 的 buf 位置
    } else {
        newsh = s_malloc(hdrlen + newlen + 1); //分配新的sds空间
        if (newsh == NULL) return NULL;
        memcpy((char *) newsh + hdrlen, s, len + 1);
        s_free(sh);  //释放旧的空间
        s = (char *) newsh + hdrlen; //定位到 buf 位置
        s[-1] = type;
        sdssetlen(s, len); //初始化 len
    }
    
    sdssetalloc(s, newlen); //初始化 alloc
    return s;
}

7、其余的API

函数名 说明
sdsempty 创建一个空字符,长度为0 内容为""
sdsnew 根据给定的C字符串创建sds
sdsdup 复制给定的sds
sdsupdatelen 手动刷新sds相关统计值
sdsRemoveFreeSpace 缩容处理,与扩容相反
sdsAllocSize 返回给定sds当前占用内存大小
sdsgrowzero 将sds扩容到指定长度,并用0填充新增加内容
sdscpylen 将C字符复制到给定sds中
sdstrim 从sds两端清除所有给定字符
sdscmp 比较两个给定sds的实际大小
sdssplitlen 按照给定的分隔符号对sds切分
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章