redis源码阅读之数据结构sds
本系列文章为结合阅读redis5.0源码以及网上查阅相关资料整理,如有错误,欢迎交流指正(QQ:2824759538)
sds是simple dynamic string的缩写,从命名上我们可以对它进行一个初步的认识,它是一个动态可扩展的字符串类型。在redis内部实现中,sds取代了C默认的字符串类型char*,在redis中,除了只会作为字符串字面常量来用的一些无需对字符串进行修改的地方之外,其他所有的字符串都是通过使用sds类型来表示。
本文通过源码对sds类型的定义、实现以及附带的一些操作进行剖析,最后通过上面的分析总结sds字符串的优缺点。
1、sds源码剖析
在redis工程文件中,sds的定义和实现相关的内容都在sds.h和sds.c文件中。
sds数据类型包括数据头header和数据内容buf两部分。
- 数据头header中存储了该字符串的长度len、最大容量alloc、header的类型flag以及空字符柔性数组
- 数据内容buf中存储了字符串数据内容。
- header和buf在内存地址上是相邻并连续的,这样做的目的提高了对sds类型的存取效率以及减少了内存碎片的产生
其结构大概的示意图如下:
1.1 sds类型定义
typedef char *sds;
从sds定义可以看出其底层类型其实就是char*,与C语言字符串保持兼容,所以在某种情况下,C标准库中对char*进行操作的函数同样也适用于sds。sds类型与C语言字符串一个最大的区别就是:
- C语言默认字符串是以字符’\0’结尾的字符数组来存储的,字符串中间不允许有’\0’字符,因为它以’\0’字符判断该字符串是否已经到达结尾,若中间有该结束符,字符串将被截断。这也是为什么C语言字符串不是二进制安全的原因。
- sds类型同样是以字符’\0’结尾的字符数组来存储的,但是它允许字符串中间有’\0’字符,因为他不是通过’\0’判断字符串结尾,而是在包头中存储了当前字符串的长度,通过该长度来判断当前字符串的大小和是否读到结尾。所以sds类型是二进制安全的。
1.2 sds header(handler)定义
为了优化redis的内存占用,适应不同长度的字符串在内存中的存储,sds header根据字符串的长度定义了5种header,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
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
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
1.2.1 包头结构分析
从header定义可以看出,除了sdshdr5没有len和alloc字段之外,其他header结构体中都包括以下几个字段:
-
len : 字符串长度,(不包括header和NULL结束符)
-
alloc:当前sds类型的最大容量(不包括header和NULL结束符)
-
flag:sds的header类型(低3位表示当前header的类型,高5位对于sdshdr5表示字符串长度,其他类型高5位没有意义),从上面宏定义,分别是0~4表示5中不同的header。
-
buf[]:柔性数组,只是一个标记作用,表示flag之后还有一个字符数组,该字符数组内存储字符串的实际内容。
宏定义SDS_HDR_VAR与SDS_HDR不难理解,SDS_HDR(T,s)将带有长度为T的header的sds s,转换为其数据头header的地址。SDS_HDR_VAR(T,s)与SDS_HDR相同,将该头的地址复制到变量sh中。
对header的定义使用了两个小技巧,分别是使用_attribute_ ((_packed_))取消编译过程中的优化对齐和使用柔性数组来保证header与数据内容在内存上是连续的。如果想要具体了解,可以参考我的两外两篇博客:
1、C语言柔性数组:保证header与data在内存分布上连续,提高sds类型的存取效率和减少了内存碎片的产生。https://blog.csdn.net/u014630623/article/details/100858910
2、pack与aligned的区别:消除了不同编译器对内存对齐优化编译的差异带来的问题https://blog.csdn.net/u014630623/article/details/88929716
3、C语言宏定义相关:记录了宏定义的一些特殊用法,https://blog.csdn.net/u014630623/article/details/88959591
1.2.2 sds类型存储示意图
sds类型数据内存存储示意图:(sdshdr5包头类型数据除外)
sdshdr5包头类型数据内存存储示意图:
字符串存储内存示意图:
如使用sds类型存储字符串"test123",初始化该字符串类型lenth(“test123”) < (1<<8),故使用sdshdr8作为sds header存储,数据在内存中存储的示意图如下:
sds* test_sds=“test123”;
其中SDS_HDR为该数据的包头地址,test_sds[-1]为flag,存储了该sds变量的包头类型,
1.3 sds重点函数分析
前面我们分析了sds类型的存储设计,接着我们分析sds相关的一些操作函数,并挑选其中几个重点的进行分析。分析方式主要是在源码中添加相关注释的方式进行分析。
1.3.1 重点函数分析
1.3.1.1 获取sds字符串长度
static inline size_t sdslen(const sds s) {
// 首先取出sds header中的flag字段s[-1],从上述示意图中可以理解知flag即为sds的前一个字符。
unsigned char flags = s[-1];
// 对flag做掩码,取出低3位,判断header的类型
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
// 若header类型为sdshdr5,字符串长度即为flag高5位
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
// 若header类型为sdshdr5之外的类型,取出header指针SDS_HDR(T,s),通过指针取出len
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;
}
1.3.1.2 根据字符串长度计算需要的header类型
/*
* 根据字符串长度计算需要的header类型
* 每个类型位小于1<<T,而不是小于(1<<T-1)的原因是alloc和len中都没有包括结尾符
*/
static inline char sdsReqType(size_t string_size) {
// 字符串长度小于5个字节,使用sdshdr5
if (string_size < 1<<5)
return SDS_TYPE_5;
// 字符串类型小于8个字节,使用sdshdr8
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
// 若LONG_MAX==LLONG_MAX,说明系统架构为64位,此时根据字符串的长度选择使用sdshdr32或sdshdr64
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
// 否则32位架构,统一使用sdshdr32
#else
return SDS_TYPE_32;
#endif
}
1.3.1.2 sds字符串的创建与销毁
/* 通过init和initlen初始化sds字符串
* 若init=NULL,则字符串的内容全部初始化为空,即所有字节都是\0
* 若init=SDS_NOINIT,则字符串的内容不做初始化。
*
* sds字符串总是以\0结尾,但是允许字符串中间出现\0
*/
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
// 首先根据字符串的长度计算需要的header类型
char type = sdsReqType(initlen);
// 若计算出类型位SDS_TYPE_5并且初始化长度为0,此时通常是需要存储变长字符串,直接使用SDS_TYPE_8类型。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 计算数据包头大小
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
// 申请内存,header+datalen+1(最后一位为结尾符\0)
sh = s_malloc(hdrlen+initlen+1);
// 若init==SDS_NOINIT,不做初始化
if (init==SDS_NOINIT)
init = NULL;
// 若init==NULL,将整片内存初始化为NULL
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
// 分配内存为空或失败,直接返回NULL
if (sh == NULL) return NULL;
// 将申请的内存偏移header len大小得到sds内存地址
s = (char*)sh+hdrlen;
// sds向前偏移一位,即为flag 指针
fp = ((unsigned char*)s)-1;
// 初始化header数据
switch(type) {
case SDS_TYPE_5:
// sdshdr5中flag高5位为字符串长度,低3位为类型header类型
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
// 初始化 header中的len,alloc以及type字段。
SDS_HDR_VAR(8,s);
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;
}
}
// 若init不空,将init中的数据拷贝到sds字符串中,二进制安全。
if (initlen && init)
memcpy(s, init, initlen);
// 数据结尾添加\0
s[initlen] = '\0';
return s;
}
/*创建一个空字符串,字符串长度为0*/
sds sdsempty(void) {
return sdsnewlen("",0);
}
/* 通过C字符串创建一个新的sds字符串 */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
/* 从sds创建一个新的字符串,深拷贝 */
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
/* sds为NULL时不做操作,其他情况计算出header指针,并销毁包括header和data在内的整块内存 */
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
/*clear只是将字符串标记为可用,并没有释放内存空间,重新复制字符串或者追加不需要*/
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
1.3.1.3 sds最大容量(alloc)扩充函数
// 该函数用于扩充sds字符串的最大可用空间,以适应sds字符串扩充长度为addlen的字符串需要
// 注意:该函数只会扩充sds的最大容量,而不会改变sds的长度len
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 计算当前sds的可用长度,即alloc-len
size_t avail = sdsavail(s);
size_t len, newlen;
// 获取sds当前的包头类型old_type
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 若当前可用空间满足新增长度addlen,则直接返回,不做任何操作
if (avail >= addlen) return s;
// 计算新的扩充的sds长度,此处使用了一个小技巧为sds预先分配内存,
// 若新的字符串长度<SDS_MAX_PREALLOC,分配2*newlen的空间
// 否则分配newlen+SDS_MAX_PREALLOC长度的空间,
// 这样的内存分配策略 既可以保证可预先分配足够的空间,又可以避免不浪费过多的内存空间。
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 重新计算新的内存空间需要使用的header类型
type = sdsReqType(newlen);
// 与sdsnew一致,尽量不使用sdshdr5,不利于扩充
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 {
// header类型改变,需要将就数据移动到新的内存中
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);
}
// 修改sds最大容量大小
sdssetalloc(s, newlen);
return s;
}
1.3.1.4 longlong转sds
// longlong带符号最长21位
#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
char *p, aux;
unsigned long long v;
size_t l;
// 对long long循环取余,放到临时字符串中
v = (value < 0) ? -value : value;
p = s;
do {
*p++ = '0'+(v%10);
v /= 10;
} while(v);
if (value < 0) *p++ = '-';
// 计算字符串的长度,并在末尾加\0
l = p-s;
*p = '\0';
// 反转字符串
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
// 首先将value转为char*,然后使用char*构造sds
sds sdsfromlonglong(long long value) {
char buf[SDS_LLSTR_SIZE];
int len = sdsll2str(buf,value);
return sdsnewlen(buf,len);
}
1.3.1 sds相关函数列表
函数原型 | 描述 |
---|---|
size_t sdslen(const sds s) | 获取sds中字符串的长度,不包括header和结尾符 |
size_t sdsavail(const sds s) | 获取sds可用空间大小,即alloc-len |
void sdssetlen(sds s, size_t newlen) | 设置sds字符串长度 |
void sdsinclen(sds s, size_t inc) | 增加字符串 |
size_t sdsalloc(const sds s) | 获取sds最大容量 |
void sdssetalloc(sds s, size_t newlen) | 设置字符串最大容量 |
int sdsHdrSize(char type) | 获取sds header大小 |
char sdsReqType(size_t string_size) | 根据字符串大小确定sds header类型 |
sds sdsnewlen(const void *init, size_t initlen) | 从init和initlen初始化sds字符串 |
sds sdsempty(void) | 创建空sds字符串 |
sds sdsnew(const char *init) | 利用C字符串创建sds |
sds sdsdup(const sds s) | 复制sds字符串,深拷贝 |
void sdsfree(sds s) | 释放sds字符串,并释放内存 |
void sdsupdatelen(sds s) | 更新字符串的长度 |
void sdsclear(sds s) | 将字符串标记为可用,不释放内存 |
sds sdsMakeRoomFor(sds s, size_t addlen) | 扩充sds字符串的内存 |
sds sdsRemoveFreeSpace(sds s) | 重新分配sds字符串,使其末尾没有可用空间 |
size_t sdsAllocSize(sds s) | 返回sds类型整个包占的总大小,包括header,data,为使用的空间和结尾符 |
void *sdsAllocPtr(sds s) | 返回sds包在内存中的首地址,即包头 |
void sdsIncrLen(sds s, ssize_t incr) | 1、incr>0 增加sds的长度,根据incr减少字符串末尾左侧的可用空间,在新的末尾处设置’\0’ 2、incr<0 缩短sds的长度,对sds字符串进行右修剪。 |
sds sdsgrowzero(sds s, size_t len) | 增长sds以使其具有指定的长度。 不属于sds原始长度的字节将被设置为零。如果指定的长度小于当前长度,则不执行任何操作。 |
sds sdscatlen(sds s, const void *t, size_t len) | 链接两个字符串 |
sds sdscat(sds s, const char *t) | 连接sds与C字符串 |
sds sdscatsds(sds s, const sds t) | 连接两个sdsd字符串 |
sds sdscpylen(sds s, const char *t, size_t len) | 复制字符串,将t复制到sds,内存不足则分配 |
sds sdscpy(sds s, const char *t) | 复制C字符串 |
int sdsll2str(char* s, long long value) | long long转C字符串 |
int sdsull2str(char *s, unsigned long long v) | unsigned longlong转C字符串 |
sds sdsfromlonglong(long long value) { | long long转sds字符串 |
sds sdscatvprintf(sds s, const char *fmt, va_list ap) | 将sds与格式化字符串va_list连接 |
sds sdscatprintf(sds s, const char *fmt, …) | sds与可变参数连接 |
sds sdscatfmt(sds s, char const *fmt, …) | 自定义format函数( %s - C String %S - SDS string %i - signed int %I - 64 bit signed integer*(long long, int64_t) %u - unsigned int* %U - 64 bit unsigned integer (unsigned long long, uint64_t) %% - Verbatim “%” character.) |
sds sdstrim(sds s, const char *cset) | 字符串裁剪字符串,从sds中删除cset字符串 |
void sdsrange(sds s, ssize_t start, ssize_t end) | 取制定范围内的sds,会修改原有sds数据 |
void sdstolower(sds s) | 字母全部转小写 |
void sdstoupper(sds s) | 字母全部转大写 |
int sdscmp(const sds s1, const sds s2) | 比较两个sds,与C比较类似 |
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) | 将字符串s以sep为分隔符进行分割,返回sds数组 |
void sdsfreesplitres(sds *tokens, int count) | 释放上述split函数的返回值中所有sds的内存 |
sds sdscatrepr(sds s, const char *p, size_t len) | 在sds后追加转移字符串,所有非打印字符将被转义,调用后原sds失效 |
int is_hex_digit(char c) | 判断字符C是否为十六进制数字 |
int hex_digit_to_int(char c) | 十六进制转十进制数字 |
sds *sdssplitargs(const char *line, int *argc) | 解析命令行参数,并将返回值结果存入sds数组中返回 |
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen) | 替换sds中指定字符串为目标字符串 |
sds sdsjoin(char **argv, int argc, char *sep) | 以特定分隔符连接c字符串存入sds |
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) | 与上述方法作用一致,分隔符不一定是C字符串 |
2、SDS优缺点分析
从上面源码分析的过程中,其实也能看出sds字符串的一些优缺点,现在做一个总结。
优点:
- sds字符串header与body在内存空间上连续,提高了字符串的存取效率,较少内存碎片的产生。
- 获取字符串长度只需要常数时间O(1)复杂度
- sds字符串可动态扩展,有效避免C传统字符串缓冲区溢出问题
- 在内存分配策略上,sds以一定策略预先分配预留空间,降低内存分配频率和避免了分配内存空间过大导致的内存浪费问题,具体策略参考sdsnewlen的实现
- sds字符串与C字符串兼容,操作char*的函数同样可用于操作sds数据(操作二进制数据可能会出现字符串丢失情况)
- 二进制安全
缺点:
- 对sds字符串进行内存重新分配后可能会导致sds指针失效,这个时候需要更新所有引用到该指针的地方,否则会发生不可预知错误