目录
字符串在 Redis 中是很常用的,Key-Value中的Key是字符串类型,Value有时也是字符串类型
Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串
C 语言字符串的缺陷
在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束
- 若在字符字段中存在“\0”,就会提前结束,造成字符转义
- 并且,C语言中strlen,获取字符串长度,就是通过字符数组中的每一个字符,并进行计数,当遇到字符为 “\0” 后,就会停止遍历,未获取正确的字符串长度
- C 语言标准库中字符串的操作函数是很不安全的,C 语言的字符串是不会记录自身的缓冲区大小的,若后续进行strcat字符串拼接操作,可能就会导致缓冲区溢出。
简单动态字符串SDS
Redis是C语言实现的,其中SDS是一个结构体,源码如下:
- len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
- alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过
alloc - len
计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。 - flags,用来表示不同类型的 SDS。
- buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
Redis设计通过不同类型的结构体(sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64)选择最适合的数据大小来存储,所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。
除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了 __attribute__ ((packed))
,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐,即取消内存对齐。
扩容机制
SDS 扩容的规则代码如下:
hisds hi_sdsMakeRoomFor(hisds s, size_t addlen)
{
... ...
// s目前的剩余空间已足够,无需扩展,直接返回
if (avail >= addlen)
return s;
//获取目前s的长度
len = hi_sdslen(s);
sh = (char *)s - hi_sdsHdrSize(oldtype);
//扩展之后 s 至少需要的长度
newlen = (len + addlen);
//根据新长度,为s分配新空间所需要的大小
if (newlen < HI_SDS_MAX_PREALLOC)
//新长度<HI_SDS_MAX_PREALLOC 则分配所需空间*2的空间
newlen *= 2;
else
//否则,分配长度为目前长度+1MB
newlen += HI_SDS_MAX_PREALLOC;
...
}
- 如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen
- 如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB,称为内存预分配
内存预分配好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用 alloc - len表示的剩余可用的空间大小 ,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
SDS优点
- 获取字符串长度的时间复杂度为O(1)
- 支持动态扩容
- 减少内存分配次数,内存扩容切换在用户态与内核态之间,切换频率过快,造成性能下降
- 二进制安全,SDS 不需要用 “\0” 字符来标识字符串结尾