在使用redis的时候经常使用到了字符串,比如下面的语句
SET user:id:100 {“name”: “zhangsan”, “gender”: “M”,“city”:"beijing"}
先来看看C语言原生的char存在的几个问题
1 存储二进制
char 是分配了一段连续的内存空间来存储字符串,并且在结尾是一’\0’,那么如果在保存数据的时候出现了\0,就会被截断,不符合保存二进制数据的需求
2 操作函数复杂度
char* 在计算长度,追加字符串的时候需要遍历整个字符串,这样子整个时间复杂度是o(N),在大量操作字符串的时候会消耗大量的时间,比如strcat的实现:
char *strcat(char *dest, const char *src) {
//将目标字符串复制给tmp变量
char *tmp = dest;
//用一个while循环遍历目标字符串,直到遇到“\0”跳出循环,指向目标字符串的末尾
while(*dest)
dest++;
//将源字符串中的每个字符逐一赋值到目标字符串中,直到遇到结束字符
while((*dest++ = *src++) != '\0' )
return tmp;
}
为了解决上面两个问题,提升效率,redis采用了sds的设计,分配sds字符串的实现
//创建SDS字符串
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
//sds字符串类型
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
//sds 头部长度
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
size_t usable;
//判断是否溢出
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
//分配空间
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 = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
//判断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 = 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;
}
}
//拷贝字符串
if (initlen && init)
memcpy(s, init, initlen);
//最后一位设置为\0
s[initlen] = '\0';
return s;
}
可以看到SDS 本质还是字符数组,只是在字符数组基础上增加了额外的元数据,所以直接用了SDS这个别名
typedef char *sds;
SDS的优势体现在
1 提升效率
在追加字符串的时候,不需要在遍历整个字符串,只要3步:
1 保证存储空间的大小 sdsMakeRoomFor
2 拷贝字符串 memcpy
3 设置目标字符串的新长度 sdssetlen
整个提升了操作字符串的效率。
//SDS追加字符串
sds sdscatlen(sds s, const void *t, size_t len) {
// 字符串原有的长度
size_t curlen = sdslen(s);
// 判断是否要扩展空间,保证目标字符串的长度够
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
// 拷贝字符串到目标字符串
memcpy(s+curlen, t, len);
// 设置目标字符串的长度
sdssetlen(s, curlen+len);
// 最后一位设置为\0
s[curlen+len] = '\0';
return s;
}
2 方便的错误检查
sdsMakeRoomFor实现了空间检查和扩容封装,避免了开发人员忘记给目标字符串扩容导致操作失败的情况,避免了没有做检查出现的内存溢出的情况。
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
//可用空间
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* Return ASAP if there is enough space left. */
//可用空间大于需要增加的空间直接返回
if (avail >= addlen) return s;
//不够空间要扩展
//原有的长度
len = sdslen(s);
//指向原有字符串的指针
sh = (char*)s-sdsHdrSize(oldtype);
//新长度
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
//判断扩展方式,是2倍的扩展,还是1倍的扩展
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
//因为增加了长度,需要判断现在的sds的类型是什么
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
//分配空间
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
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;
}
3 实现了保存二进制的需求
因为sds的头部指明了长度,不在以\0结尾计算,实现了保存二进制数据的需求。
4 节省内存的优秀设计
sds设计了5种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
区别是len和alloc的数据类型不同。
采用了__attribute__的内存分配方式,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。编译器会按需分配空间,而不是按照8字节对齐分配空间。
//__attribute__ 采用紧凑的方式分配内存,减少内存的使用
struct __attribute__ ((__packed__)) sdshdr8 {
....
}
struct __attribute__ ((__packed__)) sdshdr8 {
// 字符串现有长度 使用的数据类型uint8_t
uint8_t len; /* used */
// 字符串已分配空间 使用的数据类型uint8_t
uint8_t alloc; /* excluding the header and null terminator */
// SDS类型
unsigned char flags; /* 3 lsb of type, 5 unused bits */
// 内容
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
// 数据类型uint16_t
uint16_t len; /* used */
// 数据类型uint16_t
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
//获取SDS的类型长度
static inline size_t sdslen(const sds s) {
unsigned char flags = 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;
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;
}