前言
Redis中构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型。
作用:
- 表示字符串值
- 用作缓冲区:AOF模块中AOF缓冲区,客户端状态中的输入缓冲区
示例:
如果客户端执行命令:
redis > SET msg "hello world"
OK
那么Redis将在数据库中创建一个新的键值对,其中:
键值对的键是一个字符串对象,底层是一个保存着字符串 "msg"
的SDS
键值对的值也是一个字符串,底层是一个保存着字符串 "hello world"
的SDS
SDS定义
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 保存的字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节,空间不计算在SDS的len属性里,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作都是由SDS函数自动完成的。
好处:遵循C字符串以空字符结尾的好处是可以重用一部分C字符串函数库里的函数
SDS与C字符串的区别
(1) 提升获取字符串长度性能
- C字符串不记录自身的长度,获取C字符串长度时必须遍历整个字符串,时间复杂度为O(N);
- 而SDS在len属性中记录了SDS本身的长度,获取一个SDS长度的时间复杂度为O(1)
(2)杜绝缓冲区溢出
下面是一个字符串拼接函数
char *strcat(char *dest,const char *src);
- 由于C字符串不记录自身的长度,
strcat
假定用户在执行这个函数时,已经为dest分配足够多的内存,可以容纳src字符串中的所有内容,如果这一假设不成立,那么就会造成缓冲区溢出
示例:
假设程序里有2个在内存中紧邻的C字符串s1="Redis"
,s2="MongoDB"
,如果执行strcat(s1," Cluster")
之前忘记给s1分配足够多的空间,那么在执行strcat函数后,s1的数据将溢出到s2所在的空间,导致s2保存的内容被意外的修改了,如下图所示:
- 当SDS API中有一个用于拼接操作的
sdscat
函数,在需要对SDS进行修改时,API会先检查SDS的空间十分足够,如果不空间不够的话,会自动将SDS空间扩展至执行修改所需的大小,然后再执行实际的修改操作,所以使用SDS不需要手动修改SDS空间大小,也就不会出现缓冲区溢出问题
示例:
执行sdscat(s," Cluster")
,我们看到sdscat不仅对这个SDS进行了拼接操作,还为SDS分配了13字节的未使用空间,并且拼接之后的字符串正好是13字节长
(3)减少内存重分配次数
-
每次增长或缩短一个C字符串时,需要对这个C字符串数组进行一次内存重分配操作
- 增长字符串操作,比如拼接操作(append),在这个操作之前如果忘记内存重分配,会造成缓冲区溢出
- 缩短字符串操作,比如截断操作(trim),在这个操作之后如果忘记内存重分配来释放不再使用的空间,会造成内存泄露
-
SDS实现了空间预分配和惰性空间释放两种优化策略
- 空间预分配:当修改SDS时,会对SDS进行扩容
扩容策略:(1)修改后SDS长度小于1MB,额外分配和len大小相同的未使用空间(加倍扩容);(2)修改后长度大于等于1MB,额外分配1MB的未使用空间
注意:字符串最大长度512MB
- 惰性空间释放:当缩短SDS保存的字符串后,程序不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用
- SDS 提供了对应的API,真正的释放SDS未使用的空间,不用担心惰性释放策略会造成内存浪费。
- 空间预分配:当修改SDS时,会对SDS进行扩容
(4)二进制安全
- C字符串中的字符必须符合某种编码(比如ASCII),且除了字符串末尾外,字符串里面不能包含空字符,否则最新被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据。
- SDS的API都是二进制安全的,所有的API都会以二进制的方式来处理SDS存放在buf数组里的数据,通过使用二进制安全的SDS,使得Redis不仅可保存文本数据,还可以保存任意格式的二进制数据
(5)兼容部分C字符串函数
由于SDS字符串遵循C字符串以空字符结尾的管理,来支持复用部分C字符串函数
总结
区别项 | C字符串 | SDS |
---|---|---|
获取字符串长度时间复杂度 | O(N) | O(1) |
缓冲区溢出问题 | API不安全,可能造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
内存重分配问题 | 修改字符串长度,必然需要执行内存重分配 | 修改字符串长度,不一定需要执行内存重分配 |
存储数据类型 | 只能保存文本数据 | 可以保存文本或二进制数据 |
C字符串函数使用 | 可以使用所有的 | 可以使用一部分 |
参考文献
- 《Redis设计与实现(第二版)》-by 黄健宏