redis源码-底层数据结构之动态字符串sds

欢迎访问我的博文站点

个人站点

redis动态字符串sds简介


  首先介绍下c中的字符串,c语言中所有的"字符串"其实都是字符数组,可以使用char * str= “123”;定一个一个字符串,其中str是指向字符数组第一个字节处的地址,*str == str[0] == ‘1’,str[1] == ‘2’,str[2] == ‘3’,str[4] == ‘\0’,使用string.h中定义的strlen(str)可以获取字符数组的长度,该函数判定长度的方法是:遍历字节数组直到遇到‘\0’字节,并返回长度(不包含’\0’字节)。⚠️所以该方法的时间复杂度是O(n)线性时间。而很多字符串的方法都需要使用到长度这一属性,所以直接使用c中字符数组并不能取得好的性能。
  redis中使用了简单的数据结构定义了字符串,称为sds(simple dynamic string),其定义在sds.h中,并在sds.c中实现了方法,redis中大部分数据结构,数据库键值底层都大量使用了sds数据结构,⚠️其获取长度的时间复杂度为O(1)常数时间,并且提供了很多针对字符串的方法,下面一一介绍。

redis字符串sds底层结构-定义


  sds底层数据结构定义在sds.h中,首先通过代码片段看几个基础定义:

typedef char *sds; //⚠️实际上sds类型指向的就是实际的字符串,可以看下面各结构体中buf字符数组

//__attribute__((__packed__))⚠️是编译选项,表示该结构体内存不采用字节对齐,采用紧凑方式,也就是结构体中各字段各种该占用多少内存就占用多少内存。如struct sdshdr5就占用1个字节。
//如果申请的字节小于1<<5也就是32使用sdshdr5
struct __attribute__((__packed__)) sdshdr5{
	unsigned char flags; //⚠️对于sdshdr5,flags低三位代表类型,此处是0,高5位是buf的长度,也就是sds字符串的长度,因此根据flags可以获取长度时间复杂度是O(1)
	char buf[]; //实际sds指向的字符数组
};
//如果申请的字节小于1<<8使用sdshdr8
struct __attribute__((__packed__)) sdshdr8{
	uint8_t len; //字符串长度
	uint8_t alloc; //已分配的长度
	unsigned char flags; //低三位记录类型,高五位没有使用,后面会具体介绍flags怎么获取(其实很简单,通过上面的介绍紧凑内存模型就可以发现)
	char buf[]; //实际sds指向的字符数组
};
//如果申请的字节小于1<<16使用sdshdr16
struct __attribute__((__packed__)) sdshdr16{
	uint16_t len;  //字符串长度
	uint16_t alloc; //已分配的长度
	unsigned char flags; //第三位记录类型,高5位没有使用
	char buf[]; //实际sds指向的字符数组
};
//如果是32位系统,那么大于1<<16的都使用该结构,如果是64位的系统,当小于1ll<<32时,使用该结构
struct __attribute__((__packed__)) sdshdr32{
	uint32_t len;
	uint32_t alloc;
	unsigned char flags;
	char buf[];
};

//64位系统中,如果大于1<<32时,使用该结构
struct __attribute__((__packed__)) sdshdr64{
	uint64_t len;
	uint64_t alloc;
	unsigned char flags;
	char buf[];
}

#define SDS_TYPE_5  0 //sdshdr5类型,也就是结构体flags低三位是0
#define SDS_TYPE_8  1 //sdshdr8类型,也就是结构体flags低三位是1
#define SDS_TYPE_16 2 //sdshdr16类型,也就是结构体flags低三位是2
#define SDS_TYPE_32 3 //sdshdr32类型,也就是结构体flags低三位是3
#define SDS_TYPE_64 4 //sdshdr64类型,也就是结构体flags低三位是4
#define SDS_TYPE_MASK 7 //将flags&SDS_TYPE_MASK得到具体的sds类型
#define SDS_TYPE_BITS 3 //表示sdshdr5低三位是类型信息,高5位是字符串长度信息

通过上面对基础数据结构的分析可以发现:

  • sds - 1就可以得到flags属性,因为加了编译选项,内存是紧凑模式,所有-1就是flags字节处地址,而通过flags就可以很方便的得到长度等信息
  • 通过类型和sds指针可以获取结构体首地址,进而可以很方便获取字符串长度信息,获取长度,redis是通过宏定义完成的,后面会介绍。

创建sds动态字符串


  想要了解更多sds相关操作,最重要的莫过于创建一个sds字符串了,redis提供以下方法创建

sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init); //最终调用sdsnewlen(init, strlen(init))
sds sdsempty(void); //最终调用sdsnewlen("",0)

我们只需要看sdsnewlen方法就可以了,下面给出代码并通过注释记录每一步的含义:

//⚠️确定需要创建的字符串是什么类型,这些都在上一节中介绍过了
static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}
//⚠️根据type返回结构体字节大小,这边需要注意的是结构体中空的字符数组不占用内存,其实c中字符数组和字符指针是不同的,字符指针本身占用8字节内存(64位),但是两者在很多时候又可以相互替代。
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); //⚠️T是类型数字5、8、16等,s是buf处指针,就是实际字符串处指针,通过这个宏定义可以获取结构体指针。

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen); //确定sds类型
    //⚠️如果创建的是空字符串,type设置位SDS_TYPE_8,使用sdsempty创建的也是SDS_TYPE_8,主要是防止增大字符串需要再次额外申请内存
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type); //响应结构体字节大小
    unsigned char *fp; //指向结构体flags处指针

    sh = s_malloc(hdrlen+initlen+1); //申请字节,结构体原始大小+申请的字节大小+'\0‘
    if (init==SDS_NOINIT) //如果init指向SDS_NOINIT指向的地址,那么将init设为NULL
        init = NULL;
    else if (!init) //如果init为空字符串,将内存处字节都设置为0
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL; //如果内存申请失败,返回NULL
    s = (char*)sh+hdrlen; //s指向实际字符串位置,就是buf[]字符数组首字节处地址
    fp = ((unsigned char*)s)-1; //⚠️fp就是flags处指针,可以发现直接-1就行了,因为内存是紧凑模式的
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS); //该类型直接设置flags,可以看到高5位记录的是长度
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s); //获取结构体指针sh,可以参见上面的宏定义
            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); //将要创建的字符串拷贝至buf处
    s[initlen] = '\0';//将最后一个字节设置位'\0',这个主要是方便使用c标准库提供的很多方法
    return s; //⚠️返回buf处指针,就是实际字符串指针,而不是返回结构体指针,也就是sds是实际字符串指针。
}

获取sds长度


在说redis提供的很多方法之前,看看redis获取sds长度的方法,还是很简单的,不过redis写的很精致。

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) //获取高5位大小
static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1]; //就是*(s-1),取第一个字节处,上面有说到
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags); 
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len; //先通过宏定义转换为结构体,再返回len
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

redis针对字符串提供的方法

  redis针对sds字符串提供了大量的简便方法,如果是用c开发程序,其实完全可以拿来当库使用了,下面看看redis提供了哪些方法:

ds sdsnewlen(const void *init, size_t initlen); //上面已经说过了
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s); //复制一个字符串,其实就是调用了sdsnewlen(s, sdslen(s))
void sdsfree(sds s); //释放整个结构体内存,下面的实现可以看到。
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}
sds sdsgrowzero(sds s, size_t len); //如果len < sdslen(s),不做任何变更,如果大于,会调用sdsMakeRoomFor(sds s, size_t addlen)调整大小
sds sdscatlen(sds s, const void *t, size_t len); //合并字符串,将t中len字节字符串合并至s后,并重新设置长度
sds sdscat(sds s, const char *t); //将t合并至s后,并重新设置长度
sds sdscatsds(sds s, const sds t); //将t连接至s处,⚠️此时t的内存并没有释放,如果不需要了要手动释放
sds sdscpylen(sds s, const char *t, size_t len);//将t处len字节复制到s中,如果s当前分配的内存不够,需要调用sdsMakeRoomFor扩大内存,再拷贝,并重新设置长度
sds sdscpy(sds s, const char *t); //将t拷贝至s,并重新设置长度

sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...) //将格式化的字符串写到s处
    __attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);
sds sdstrim(sds s, const char *cset); //删除s中前后包含在cset中的字符
void sdsrange(sds s, ssize_t start, ssize_t end); //获取包含在start到end的子字符串,包含start和end字符,start和end都可以是负数
void sdsupdatelen(sds s); //更新字符串长度,可以将字符串后面的一个子串截断
void sdsclear(sds s); //将字符串长度设置为0,并将s[0]设置为'\0'
int sdscmp(const sds s1, const sds s2); //比较字符串,内部调用了memcmp函数
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count); //将s按照sep分隔,并返回sds数组
void sdsfreesplitres(sds *tokens, int count); //释放sds数组
void sdstolower(sds s); //sds转换为小写
void sdstoupper(sds s); //sds转换为大写
sds sdsfromlonglong(long long value); //将value转换为sds
sds sdscatrepr(sds s, const char *p, size_t len); //将p中一些特殊字符拼接到s后
sds *sdssplitargs(const char *line, int *argc);
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen); //将s的一个非连续字串from替换成to
sds sdsjoin(char **argv, int argc, char *sep); //将argv字符串数组拼接,按照sep拼接
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);//和上面类似

sds sdsMakeRoomFor(sds s, size_t addlen); //调整分配内存大小
void sdsIncrLen(sds s, ssize_t incr); //调整长度
sds sdsRemoveFreeSpace(sds s); //释放s中的空闲内存
size_t sdsAllocSize(sds s); //已分配内存大小:结构体大小 + 字符串分配内存大小 + 1
void *sdsAllocPtr(sds s); //返回结构体处指针

void *sds_malloc(size_t size); //分配内存
void *sds_realloc(void *ptr, size_t size); //重新分配内存
void sds_free(void *ptr); //释放内存

总结


redis源文件sds.c中提供了测试sds的例程,可以看看,需要设置SDS_TEST_MAIN宏定义。
通过上面对源代码分析和注释可以看出来,redis中sds字符串提供了以下便利性:

  • 常数时间获取字符串长度的方法
  • 杜绝缓冲区溢出,主要是redis会判断当前分配的内存是否足够,如果不够的话会扩大内存,防止例如sdscat之类的函数造成缓冲区溢出。
  • 空间预分配,防止频繁修改造成的内存分配次数频繁,这个没有分析,可以到源码中看看,就是在执行sdsMakeRoomFor方法时,扩展的内存会比需要的内存大,策略如下:
#define SDS_MAX_PREALLOC (1024*1024) //1M大小
newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) //不满1M,分配需求大小*2
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC; //满1M,分配需求大小+1M
  • 惰性释放,就是在执行例如sdstrim方法时,并不会把剩余的空间释放,这样的话下次再对该字符串操作就可以直接利用剩余内存了,可以通过sdsRemoveFreeSpace释放剩余空间。
  • sds可以方便的使用c标准库中的函数,因为sds就是指向实际字符串的,并且末尾也是以’\0’结尾的。
  • sds可以存储包含’\0’的字符串,因为redis是通过len记录长度的,其实可以通过sds sdsnewlen(const void *init, size_t initlen)方法直接创建就行了,init中可以包含’\0’字节。
  • redis源码极简,写的很完美,值得学习借鉴。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值