Redis源码解析 | sds
SDS
头文件及作用
- sds.h: sds声明- sdsalloc.h: 为sds分配内存
源码文件
sds.h
中有这样一行代码
typedef char *sds;
很清晰、明了,sds
其实就是char*
。
最新的6.2分支的代码:
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
最初的模样
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会检查未使用的空间是否够用,如果够用则直接使用未使用的空间,无须执行内存重分配。如果不够用则重新分配内存:
![03eee2e2c83b2aabb255efde21e72a52.png](https://img-blog.csdnimg.cn/img_convert/03eee2e2c83b2aabb255efde21e72a52.png)- 内存释放策略:在`SDS`进行字符串缩短操作时,程序不会立马重新分配内存来缩短多出来的字节,而是使用属性`free`记录下来等待将来使用。通过惰性内存释放策略,`SDS`避免因缩短字符串操作而进行内存重新分配的次数,为将来有可能的增长操作带来了优化。- 可以通过sds api来释放未使用的空间,不用担心惰性空间释放策略会造成内存浪费
二进制安全
- 为了确保redis可以保存二进制数据(图片、视频等),SDS的API是二进制安全的。 程序不会对其中的数据做任何的限制,过滤,数据存进去是什么样子,读出来就是什么样子,这也是buf数组叫做字节数组而不是叫字符数组的原因。Redis不仅可以保存文本数据,还可以保存任意格式是二进制数。-
C
字符串中除了末尾的空字符,字符串其他位置不能包含空字符,所以C语言字符串只能保存文本数据,不能保存二进制数据。 总结:
6.2分支的SDS
我们回到6.2的分支上,可以看到五个结构体可以归纳为一种包含Header
与数据包
的结构体。
想要得到sdshdr
的属性,需要知道header
类型,flags
字段存储了header
类型,假如我们定义了sds* s
,那么获取flags
字段仅仅需要将s向前移动一个字节,即unsigned char flags = s[-1]
, header
的五类型如下:
#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进行操作:
#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]));
}
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2021/01/sds/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接