redis源码——SDS

SDS

     redis中和SDS有关的代码在sds.h sds.cpp sdsalloc.h三个文件中,SDS是redis数据类型中string类型的底层实现,string类型可以用来存放字符串或者整数,当为纯整数时可以进行

结构体

     redis的定义了5种长度SDS的结构体分别是{ 5 , 8 , 16 , 32 , 64 },这五种结构中5是不再使用的,所以实际上只有4种结构。如果声明时出现5的结构会转换成8的结构,4种结构体的内容是类似的,5会有所不同,下面会给出5和8的结构体

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags;// 1字节8位,3位表示字符串类型,5位表示字符串长度
    char buf[];         // 存放字符串的字符数组
};
struct __attribute__ ((__packed__)) sdshdr8 {
	/*
       uint8_t是一个宏定义,其中数字8表示这种类型占8位也就是1字节,其实就
   是char类型
       16位中为uint16_t,表示占16位2字节,也就是short类型,32位与64位同理
       这里仅仅是len的数据类型,即字符串长度大小,与使用的字符编码无关
    */
    uint8_t len;        // 字符串的实际长度
    uint8_t alloc;      // 给字符串分配内存的大小
    unsigned char flags;// 1字节8位,3位表示字符串类型,5位未使用
    char buf[];         // 存放字符串的字符数组
};

新建字符串

     SDS的新建过程主要分为以下几个步骤:
       1、判断类型,计算出合适的大小
       2、分配内存,初始化结构体
       3、返回字符串,这里返回的是字符串本体,而非结构体

/**
*	sds是redis定义的宏,表示 char*
*	sdshdr是redis定义的sds结构体
*	init为初始化的字符串内容
*/
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
	// 用来指向sds结构体的指针
    void *sh;
    // 用来指向字符串的指针
    sds s;
    // 根据指定的initlen判断应该声明哪种结构体-> 5,8,16,32,64
    char type = sdsReqType(initlen);
	// 如果类型判断是5的话,强制变为8,此处可以看出5长度的结构体是不使用的
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 根据类型计算出结构体长度
    int hdrlen = sdsHdrSize(type);
    // 指向结构体种flags的指针
    unsigned char *fp;
    // 表示分配内存时,可用空间的大小
    size_t usable;
	/* 断言判断是否会类型溢出,initlen类型为size_t,是long long类型的宏定义
	   申请的空间大小是initlen+hdrlen+1,如果initlen的长度过长,会导致计算结果溢出变为负数
	 */
    assert(initlen + hdrlen + 1 > initlen);
    // 分配内存空间,付给结构体指针
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    // 如果分配失败返回NULL
    if (sh == NULL) return NULL;
    /*
    	判断初始化内容
    	如果初始化内容为SDS_NOINIT,则不进行初始化
    	如果init为NULL,则全部初始化为零
    */
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    // 获取到字符串数组的地址 buf[]的首地址
    s = (char*)sh+hdrlen;
    // 通过s[-1],获得flags的地址
    fp = ((unsigned char*)s)-1;
    // 计算出当前申请的buf[] 的可用空间,如果超过最大值,设置为最大值
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    // 通过type判断出应该初始化哪种结构体
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
        	/*
        	宏定义:struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)))
        	 通过s的地址计算出结构体首地址并赋值给sh
        	*/
            SDS_HDR_VAR(8,s);
            // 初始化字符串长度
            sh->len = initlen;
            // 初始化申请空间大小
            sh->alloc = usable;
            // flags赋值为type
            *fp = type;
            break;
        }
        .......
    }
    // 如果init不为NULL,就将init中的内容赋值给s即结构体中的buf
    if (initlen && init)
        memcpy(s, init, initlen);
    // 设置'\0'字符串结束标志
    s[initlen] = '\0';
    // 返回s,即字符串本体
    return s;
}

增量扩容

     redis在扩容时会先获取出当前的一些数据包括:剩余空间大小、当前的结构体类型,如果当前剩余空间仍然大于想要的新空间,将不会有任何操作。只有当剩余空间不够使用时才会进行扩容。

/**
* s当前字符串,addlen新增的长度(注意是新增的长度不是增加后的总长度)
* greedy有两个值,当greedy为1得时候表示不仅申请addlen空间,还会申请额外得空间
* 当greedy为0的时候,仅仅申请addlen所需的空间
*/
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
	// 旧结构体指针,新结构体指针
    void *sh, *newsh;
    // 计算剩余得空间大小,即alloc - len
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    /* 
       s[-1]就是flags,flags的低3位表示了当前的采用的是那种结构体,
       SDS_TYPE_MASK,翻译过来就是类型掩码,值为7,和flags进行与位运算之后
       就可以将flags的高5位清零,保留低3位赋值给oldtype
    */
    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);
    // 计算出新的字符串的长度
    reqlen = newlen = (len+addlen);
    // 断言判断newlen是否溢出,如果len或者addlen较大,两者相加可能超出size_t所表示的最大值
    assert(newlen > len);
    /*
     如果greedy为1,就多申请空间
     SDS_MAX_PREALLOC 宏定义 1024*1024即1MB
     当新的长度小于1MB的时候就2倍扩容(注意是新容量的2倍)
     当长度大于1MB的时候就每次增加1MB
    */
    if (greedy == 1) {
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
    }
	// 获取新容量所应申请的结构体类型
    type = sdsReqType(newlen);
	// 如果是5位的类型就转换为8
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
	// 计算出相应的空结构体所需要的空间
    hdrlen = sdsHdrSize(type);
    // 断言判断溢出
    assert(hdrlen + newlen + 1 > reqlen);
    if (oldtype==type) {
        // 如果新的类型等于旧的类型,就在当前结构体上进行增容
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } 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;
}

空间整理

     sds的空间整理,根据需要将结构体增大或者缩小,以充分利用内存

/**
* s要整理的字符串数组
* size目标空间大小,注意此处与上面addlen的区别,从名称就可以看出addlen表示的是新增长度
* size表示的新空间的总长度
* 
*/
sds sdsResize(sds s, size_t size, int would_regrow) {
	// 旧结构体指针、新结构体指针
    void *sh, *newsh;
    // 计算旧类型
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    // 旧的结构体类型的大小
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    // 当前的字符串长度
    size_t len = sdslen(s);
    // 计算出旧结构体的地址
    sh = (char*)s-oldhdrlen;

    // 如果已经申请的空间大小正好等于目标空间大小则不进行操作
    if (sdsalloc(s) == size) return s;

    // 如果目标空间长度小于当前长度旧进行截断
    if (size < len) len = size;
	// 计算出新的结构类型
    type = sdsReqType(size);
    if (would_regrow) {
        // 将5长度类型转换为8
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    }
    // 计算出新结构体的大小
    hdrlen = sdsHdrSize(type);

    /* 如果类型相同或者新的类型比旧的类型所占空间比较小,就在旧的空间上进行改造,
     *  否则的话就申请一块新的空间
     */
    int use_realloc = (oldtype==type || (type < oldtype && type > SDS_TYPE_8));
    size_t newlen = use_realloc ? oldhdrlen+size+1 : hdrlen+size+1;
    int alloc_already_optimal = 0;
    #if defined(USE_JEMALLOC)
        // je_nalloc返回的是预期分配的内存大小,如果和当前空间分配相等,就直接不分配空间
        alloc_already_optimal = (je_nallocx(newlen, 0) == zmalloc_size(sh));
    #endif
	// 检查是否小于或等于当前结构体,如果是,就在原空间进行改造
    if (use_realloc && !alloc_already_optimal) {
        newsh = s_realloc(sh, newlen);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else if (!alloc_already_optimal) {
    	// 如果不是,就申请新的空间将内容复制过去
        newsh = s_malloc(newlen);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len);
        // 释放旧的空间
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
    }
    // 设置结构体数据
    s[len] = 0;
    sdssetlen(s, len);
    sdssetalloc(s, size);
    // 返回新的字符串指针
    return s;
}

字符串长度增加(或减少)

     这个是redis的字符串长度增加函数,上面两个都是对空间进行修正,而这个函数是对字符串本身进行操作

/**
* s要操作的字符串
* incr字符串要增加的长度(可以为负值对字符串进行裁剪)
*/
void sdsIncrLen(sds s, ssize_t incr) {
	// 获取当前字符串所在结构体的flags
    unsigned char flags = s[-1];
    // 最终的长度
    size_t len;
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
        	// 由于5位长度的结构体flags低3位表示类型高5位表示长度,所以这里是对flags进行调整
            unsigned char *fp = ((unsigned char*)s)-1;
            unsigned char oldlen = SDS_TYPE_5_LEN(flags);
            assert((incr > 0 && oldlen+incr < 32) || (incr < 0 && oldlen >= (unsigned int)(-incr)));
            *fp = SDS_TYPE_5 | ((oldlen+incr) << SDS_TYPE_BITS);
            len = oldlen+incr;
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr)));
            len = (sh->len += incr);
            break;
        }
        ......
        default: len = 0;
    }
    s[len] = '\0';
}

字符串拼接

     SDS字符串拼接函数,先通过_sdsMakeRoomFor函数申请空间,然后将新的字符串写入进去,其余字符串的操作(字符串复制等)都是类似的。另外redis自己也实现了基于SDS的其他常见的字符串常见函数,比如格式化、字符串比较等

/**
* t是指向要增加的字符串指针
*/
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);
	// 申请更大的空间
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    // 把t中内容复制到s的末尾
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    // 字符串结尾标志
    s[curlen+len] = '\0';
    return s;
}

总结

     redis数据类型string的底层实现是sds,之所以没有采用c语言中的字符串操作可能有以下几个原因:

1、C语言不提供数组的越界检查,如果越界可能导致程序崩溃
2、C语言中使用的字符串函数都是检查'\0'作为字符串的结束标志,当用户的字符串中出现该标志时,就会出现字符串截断现象
3、也是由于C语言检查'\0'作为结束标志,导致每次查询长度时都需要遍历一遍才可以获取到长度,当字符串长度较大时,效率低下
4、C语言字符串扩容时效率不高,性能差

     SDS在初始化时会申请正好长度的空间,而有扩容需要时,才会判断是否多申请空间。申请空间时,如果大小小于1MB,就会二倍扩容,如果大于1MB就会每次增加1MB

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值