企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## `SDS`头文件及作用 * sds.h: sds声明 * sdsalloc.h: 为sds分配内存 源码文件`sds.h`中有这样一行代码 ```cpp typedef char *sds; ``` 很清晰、明了,`sds`其实就是`char*`。 最新的6.2分支的代码: ```cpp 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[]; }; ``` >`__attribute__ ((__packed__)) `的设置是告诉编译器取消字节对齐,则结构体的大小就是按照结构体成员实际大小相加得到的。 Redis是在3.2版本(包括3.2)之后把`sdshdr`改为现在这样的。 看到这五个结构体有点懵,我想程序都有个初始版本,万变不离其宗,我切换到2.2版本的分支上,看到了`sdshdr`最初的模样 ```cpp struct sdshdr { int len; int free; char buf[]; }; ```  注意:这里的len是buf字符数组中,不包括最后的空字符的字符个数。`sdshdr` 是一个包含字符串数组和长度的结构体。 ## 相比`C`字符串,`SDS`的优势 `sdshdr`和C字符串有什么优势和劣势呢?请继续往下看 ### 获取字符串的时间复杂度 * `SDS`字符串: O(1) * `C`字符串:O(n),需要遍历字符串,以`\0`结尾 * 使用SDS可以确保获取字符串长度的操作不会成为Redis的性能瓶颈。 ### 杜绝缓冲区溢出 * `C`字符串不会记录自身的长度和空闲空间,容易造成缓冲区溢出,而SDS则不会,在拼接字符串之前,会通过`free`字段检测是否能满足数据的存放,如果不满足则会进行扩容。 ### 减少修改字符串时带来的内存重分配次数 * `C`字符串在对字符进行拼接或者缩短的情况下,都会对这个`C`字符串的内存进行重新分配。比如拼接字符串时,需要重新分配来扩展原有字符串数组的大小,避免溢出缓冲区;在对字符串进行缩短操作时,需要重新分配内存来释放不需要的那部分,避免内存泄漏。所以C语言中每次修改字符串都会造成内存重分配。 * `SDS`字符串则有`len`和`free`属性,可以实现两种内存分配和释放操作:**内存预分配和内存惰性释放** * 在对`SDS`进行扩展的时候,程序不仅会为`SDS`分配所必需的内存,还会为`SDS`分配额外的空闲内存。这样就减少连续增长字符串所需内存重新分配的次数。通过内存预分配,`SDS`将`N`次字符串增长操作所需内存分配的次数从必须`N次降低为最多`N`次。 * 额外未分配的内存的大小的策略:在扩展sds空间之前,sds api会检查未使用的空间是否够用,如果够用则直接使用未使用的空间,无须执行内存重分配。如果不够用则重新分配内存: ![](https://img2020.cnblogs.com/blog/1477786/202006/1477786-20200606160640123-1895539316.jpg) * 内存释放策略:在`SDS`进行字符串缩短操作时,程序不会立马重新分配内存来缩短多出来的字节,而是使用属性`free`记录下来等待将来使用。通过惰性内存释放策略,`SDS`避免因缩短字符串操作而进行内存重新分配的次数,为将来有可能的增长操作带来了优化。 * 可以通过sds api来释放未使用的空间,不用担心惰性空间释放策略会造成内存浪费 ### 二进制安全 * 为了确保redis可以保存二进制数据(图片、视频等),SDS的API是二进制安全的。 程序不会对其中的数据做任何的限制,过滤,数据存进去是什么样子,读出来就是什么样子,这也是buf数组叫做字节数组而不是叫字符数组的原因。Redis不仅可以保存文本数据,还可以保存任意格式是二进制数。 * `C`字符串中除了末尾的空字符,字符串其他位置不能包含空字符,所以C语言字符串只能保存文本数据,不能保存二进制数据。 总结: ![](https://img2020.cnblogs.com/blog/1477786/202006/1477786-20200606161043783-663584875.jpg) ## 6.2分支的`SDS` 我们回到6.2的分支上,可以看到五个结构体可以归纳为一种包含`Header`与`数据包`的结构体。 ![](https://img2020.cnblogs.com/blog/1477786/202006/1477786-20200606161427008-301082430.png) 想要得到`sdshdr`的属性,需要知道`header`类型,`flags`字段存储了`header`类型,假如我们定义了`sds* s`,那么获取`flags`字段仅仅需要将s向前移动一个字节,即`unsigned char flags = s[-1]` , `header`的五类型如下: ```cpp #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 ``` 然后通过以下宏定义来对header进行操作: ```cpp #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))); // 获取header头指针 #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 获取header头指针 #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 获取sdshdr5的长度 ``` ## SDS的创建 & 扩容 & 销毁 创建一个sds字符串函数: ``` sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); } ``` 触发扩容操作: ``` sds sdscatfmt(sds s, char const *fmt, ...) { ... switch(*f) { case '%': next = *(f+1); f++; switch(next) { case 's': case 'S': str = va_arg(ap,char*); l = (next == 's') ? strlen(str) : sdslen(str); if (sdsavail(s) < l) { //当剩余空间小于要加入数据的大小时需要扩容 s = sdsMakeRoomFor(s,l); //扩容 } memcpy(s+i,str,l); sdsinclen(s,l);//设置 已使用字符长度 属性。 i += l; break; ... } f++; } va_end(ap); /* Add null-term */ s[i] = '\0'; return s; } ``` 惰性删除操作, 设置 `len`字段为0,但是并没有释放内存。这是一个比较极端的例子, ``` void sdsclear(sds s) { sdssetlen(s, 0); s[0] = '\0'; } ``` 惰性删除操作,这个例子比较正常,如果新数据的长度小于等于当前数据的长度则不进行扩容,不扩容只需要修改`len`这个字段的值即可,不需要释放内存。反之则需要进行扩容。 ``` sds sdsgrowzero(sds s, size_t len) { size_t curlen = sdslen(s); if (len <= curlen) return s; //新数据的长度小于等于数据的长度则不进行扩容。 s = sdsMakeRoomFor(s,len-curlen); //扩容操作 if (s == NULL) return NULL; /* Make sure added region doesn't contain garbage */ memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */ sdssetlen(s, len); //设置已使用数据长度,len字段 return s; } ``` 释放空闲空间 ``` sds sdsRemoveFreeSpace(sds s) {} ``` 销毁`SDS` ``` void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); } ```