源码版本:REDIS 5.0.4
redis没有直接使用C语言当中以’\0’的结尾的字符串,而是实现了自己字符串结构简单动态字符串(simple dynamic string, SDS),同时SDS又保留了字符串当中的‘\0’兼容C语言字符串
SDS结构体类似如下:
struct __attribute__ ((__packed__)) sdshdrX
{
length_type len;
length_type alloc;
unsigned char flags;
char buf[];
};
结构体分析:
len代表已使用的缓存,不包括字符串中的‘\0’;
alloc代表已分配的总长度;
sdshdrX当中的X可以为8、16、32、64(其实还有5,redis当中sdshdr5没有做使用,只是起标记作用),与之对应的length_type为uint8_t、uint16_t、uint32_t、uint64_t,length_type的不同意味着起所能存储的字符串最大长度的不同;
flags标识当前SDS字符串length_type的数据类型;
__attribute__ ((__packed__))的作用是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,结构体在内存当中紧密排列。
SDS内存结构如下:
SDS字符串创建函数
sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sdsnewlen、sdsnew会根据传入init参数创建SDS字符串,sdsempty会创建一个空的SDS字符串,三个函数最终的具体实现均由sdsnewlen来具体实现,函数返回值数据类型为char *指针(typedef char *sds),起指向的位置为buf的起始地址。
sdsnewlen函数内部流程:
假设执行sds test = sdsnewlen(“Hello world!”, 12);前产生的SDS字符串结构如下:
SDS字符串拼接函数
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
sds sdscatsds(sds s, const sds t);
函数的最终实现均由sdscatlen完成,其函数流程如为:
IF 不需要重新分配内存
ELSE
IF 两个字符串长度之和小于SDS_MAX_PREALLOC
新长度=字符串长度之和
ELSE
新长度=字符串长度之和+SDS_MAX_PREALLOC
IF length_type没有发生变更
realloc新长度内存
ELSE length_type发生变更
Malloc新长度内存
更新SDS类型
拼接字符串
SDS字符串拷贝函数
sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);
内部流程与SDS字符串拼接函数类似
此外还有如下的中功能的函数
sds sdscatfmt(sds s, char const *fmt, ...);//格式化字符串
sds sdstrim(sds s, const char *cset); //去掉两端重复字符
void sdstolower(sds s);//字母转小写
void sdstoupper(sds s);//字母转大写
int sdscmp(const sds s1, const sds s2); //比较函数
SDS计算长度、拼接字符等时间复杂度为O(1),C字符串的复杂度为O(n),SDS有空间预分配,可以减少内存分配次数,但也由此可能造成内存浪费的现象,但因为SDS的内存分布策略,内存浪费较低。
因为SDS字符串最后一个字节也复制值为’\0’,并且sds指针指向的起始位置为字符串起始位置所以,SDS字符串也可以使用C的字符串函数,例如printf。
C字符串不记录自身长度, 修改字符串时不会判断本身是否拥有足够的内存空间, 当内存空间不足时, 则会造成缓冲区的溢出,SDS对字符串进行修改时,先检查内存空间是否满足修改的需要, 若不满足, 则自动扩展SDS的内存空间. 所以使用SDS既不需要手动修改内存空间的大小, 也不会出现缓冲区溢出的情况