redis源码解读(一):基础数据结构之SDSgithub
Redis没有直接使用c语言的字符串,而是本身定义了一个字符串数据结构,SDS做为默认的字符串,咱们设置的全部键值基本都是SDSredis
C语言字符串特色:数组
strlen(s)
的时间复杂度为O(n)
\0
终止符断定一个字符串的结尾,这种规则使得C语言的字符串是二进制不安全的那么从性能考虑,上面三个问题能够这么解决:安全
O(1)
时间复杂度的长度查询:
length
属性,再也不须要以某种特殊格式(\0
)解析数据,因此二进制安全了github 源码src/sds.h,结构体声明代码以下:bash
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
// 经过buf获取头指针
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
复制代码
上方虽然声明了sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
、sdshdr64
五种类型,但均可以归纳为:数据结构
len
记录当前字节数组的长度不包括\0
alloc
记录了当前字节数组总共分配的内存大小,不包括\0
flags
记录了当前字节数组的SDS_TYPE
buf
保存了字符串真正的值以及末尾的一个\0
看看一个sdshdr8
的实例, 整个SDS的内存是连续的,统一开辟的,经过这样的方式就能经过buf头指针进行寻址,拿到整个struct的指针curl
编译器内存对齐的优化策略:struct的分配的内存是内部最大元素的整数倍性能
其中__attribute__ ((__packed__))
的做用为:告诉编译器不要对这个结构体进行优化对齐,让结构体内部的字段与字段之间紧挨在一块儿优化
printf("%ld\n", sizeof(struct sdshdr8)); // 3
printf("%ld\n", sizeof(struct sdshdr16)); // 5
printf("%ld\n", sizeof(struct sdshdr32)); // 9
printf("%ld\n", sizeof(struct sdshdr64)); // 17
复制代码
以sdshdr32
为例,其内部最大元素为4(uint32_t
占4字节),不进行内存对齐,节省了4*3 - 9 = 3
字节,同理,sdshdr64
节省了8*3 - 17 = 7
字节。
在绝大多数场景下,没有开发者会给key取一个特别长的名字,将这些key变成sds字符串,就要在sdshdr.len
中存放这些字符串的长度,如何选择len
的类型?
uint8
: 确定会有字符串的长度超过2^8 - 1uint16
: 确定会有字符串的长度超过2^16 - 1uint32
: 确定会有字符串的长度超过2^32 - 1uint64
: 99%的状况下字符串长度都是很是简短的,用8个字节来存长度,极端浪费所以在建立时,先计算出字符串的长度,根据长度,把sdshdr
分为几种类型,达到节省内存的效果,能够看到另外一个小细节:sdshdr5
直接省掉了len
字段, 用高5位存放长度,低3位存放类型,因此后面的结构有/* 3 lsb of type, 5 unused bits */
这样的注释
调用:
mysds = sdsnewlan("abc", 3);
复制代码
解析见注释
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
// 根据内容长度`initlen`,肯定`SDS_TYPE
char type = sdsReqType(initlen);
// 空字符串使用SDS_TYPE_8类型,由于空字符串一般用于追加操做,SDS_TYPE_5不适合
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 获取结构体大小
int hdrlen = sdsHdrSize(type);
// flags pointer
unsigned char *fp;
// 分配内存:结构体大小+字符串大小+1(`\0`)
sh = s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
// 空字符串初始化内存为0
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
// 得到flags指针
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
// SDS_TYPE_5的flags字段前5位保存长度后3位保存type
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s); // 得到sdshdr的指针
sh->len = initlen; // 设置len
sh->alloc = initlen; // 设置alloc
*fp = type; // 设置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'; // 字符数组最后一位设置为\0
return s;
}
复制代码
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); // 更新len属性
s[curlen+len] = '\0'; // 末尾追加一个\0
return s;
}
复制代码
重点在于sdsMakeRoomFor
, 经过策略,减小拼接操做的内存分配次数
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 获取可用长度,即sh->alloc - sh->len;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 剩余空间足够,无需扩容,返回
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
// 分配策略:小于1mb,内存翻倍,不然多分配1m
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 对于SDS_TYPE_5有一句注释:sdshdr5 is never used
type = sdsReqType(newlen);
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 对比扩容先后类型是否改变,作对应的处理,不重要
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
newsh = s_malloc(hdrlen+newlen+1);
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);
}
sdssetalloc(s, newlen);
return s;
}
复制代码
sds->len
将获取字符串长度的时间复杂度下降到了O(1)
,进而使得字符串不受限于C字符串的\0
终止符,实现二进制安全sds->buf
的末尾追加一个\0
,在部分场景下和C语言字符串保持一样的行为