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