为什么Redis选择SDS
1、缓解C语言字符串的缺陷
在 C 语言中可以使用 char* 字符数组来实现字符串。每个字符串分配一段连续的内存空间,依次存放字符串中的每一个字符,最后以null字符结尾。这种设计存在以下问题:
1、低效的操作
每次获取字符串长度都需要遍历字符串,找到null字符的位置,时间复杂度为O(n)。
2、二进制不安全
C字符串不能存储二进制数据,假如要存储的二进制图片中包含了null字符,会把它看做是字符串的结尾,出现错误。
3、缓冲区溢出
字符串操作(如拼接、复制)容易导致缓冲区溢出,进而引发安全问题。
2、提升性能和灵活性
SDS设计解决了传统C字符串存在的问题,提供了更高效、更安全的字符串操作能力,主要体现在以下几个方面:
1) 获取字符串长度的高效性
传统的C字符串需要线性遍历来获取长度,时间复杂度为O(n)。而SDS在结构体中维护了len字段,可以在常数时间O(1)内直接获取字符串长度,大大提升了效率。
2) 防止缓冲区溢出的安全性
C字符串在拼接、复制等操作时,很容易出现缓冲区溢出的问题,从而导致安全隐患。SDS则通过动态分配内存的方式,在字符串操作时自动调整内存大小,从根本上避免了缓冲区溢出。
3) 存储二进制数据的灵活性
传统C字符串由于使用null字符作为结尾标记,无法安全地存储包含null字符的二进制数据。而SDS则完全绕过了这一限制,可以方便地存储任意二进制数据,提高了数据存储的灵活性。
4) 空间预分配的优化
在对SDS进行扩展时,SDS会预先分配更大的内存空间,减少了频繁重新分配内存所带来的性能开销。当字符串长度小于1MB时,扩容后的空间是原长度的两倍;当长度超过1MB时,则会额外分配1MB的空间,防止过度使用内存。
SDS结构
在Redis源码中,SDS结构源码(位于sds.h
文件)如下:
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[];
};
定义了sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
和sdshdr64
多种结构,sdshdr5
不再使用无需关注。每一个SDS结构都包含了实际存储字符串的字符数组buf[],记录当前字符串的长度len、记录分配的总空间alloc(不包括头部和空终止符) 和 SDS类型flags。
它们的主要区别就在于,数据结构中的字符数组现有长度 len 和分配空间长度 alloc,这两个元数据的数据类型不同。uint8_t 是 8 位无符号整型,会占用 1 字节的内存空间。当字符串类型是sdshdr8
时,它表示字符数组buf[]
长度(包括数组最后一位\0)不会超过 256 字节(2 的 8 次方等于 256)。uint16_t、uint32_t、uint64_t,分别表示不超过 2 的 16 次方、32 次方和 64 次方。这两个元数据各自占用的内存空间在 sdshdr16、sdshdr32、sdshdr64 类型中,则分别是 2 字节、4 字节和 8 字节。
在SDS结构的定义上还使用了__attribute__ ((__packed__))
,来优化空间,它的作用就是告诉编译器,在编译结构时,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。
SDS的内存分配过程
SDS的内存分配包括创建、扩展和释放三个主要过程。以下是SDS的内存管理源码分析。
1、SDS创建
SDS的创建函数是sdsnewlen
。
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
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;
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);
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);
s[initlen] = '\0';
return s;
}
1.1 确定类型和头部大小
首先,通过sdsReqType
函数确定适合的SDS类型,然后计算头部大小。sdsReqType
根据字符串长度选择合适的SDS类型,而sdsHdrSize
返回相应类型的头部大小。
char type = sdsReqType(initlen);
int hdrlen = sdsHdrSize(type);
1.2. 分配内存
接着,通过s_trymalloc_usable
或s_malloc_usable
函数分配内存,这里&usable
会返回实际可用的内存大小。
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
1.3. 初始化内存
根据init
参数的不同,初始化内存。
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
1.4. 设置类型和长度
最后,设置SDS的类型标志和长度,并返回字符串的指针。s
表示是SDS中buf[]
的起始位置,len
表示使用空间,alloc
表示分配的空用空间。
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
2、SDS扩容
SDS的扩展函数是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 */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
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;
}
2.1 计算可用空间
首先,计算当前字符串的可用空间和长度。
size_t avail = sdsavail(s);
size_t len = sdslen(s);
2.2 计算新长度
如果可用空间不足,则需要扩展。新长度通常是当前长度的两倍,以减少频繁的内存分配操作。SDS_MAX_PREALLOC
是1M的大小,扩容后新空间的大小有两种情况:
- 如果新长度小于1M,则新空间为扩容后字符串长度的两倍+1
- 否则,则新空间为扩容后字符串长度+1M+1
后面的+1
是调用内存分配函数是添加的。
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
2.3. 分配新内存
根据新长度分配内存。如果新的类型与旧的类型相同,直接使用s_realloc_usable
重新分配内存,否则需要创建新的SDS,重新分配内存拷贝数据。
type = sdsReqType(newlen);
hdrlen = sdsHdrSize(type);
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);
}
2.4. 更新长度
最后,更新分配的总空间,并返回扩展后的SDS。
sdssetalloc(s, usable);
return s;
3、SDS的释放
SDS的释放函数是sdsfree
。
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
3.1. 检查空指针
首先,检查传入的指针是否为空。
if (s == NULL) return;
3.2. 释放内存
通过s_free
函数释放内存,注意需要将指针偏移到SDS头部的位置。
s_free((char*)s-sdsHdrSize(s[-1]));
总结
Redis使用简单动态字符串SDS来代替传统C字符串,解决了获取长度低效、缓冲区溢出和二进制不安全等问题。SDS在结构体中维护长度,支持二进制存储,自动扩容防止溢出,性能和灵活性均有提升。文中详细分析了SDS的内存分配、扩容和释放过程。