本篇主要对redis的数据结构进行总结,主要包括SDS(simple dynamic string)的实现的介绍、为什么Redis采用SDS而不是C字符串。
1、SDS的结构实现及定义

- free属性 -- 记录buf数组中未使用字节的数量
- len属性 -- 记录buf数组中已使用字节的数量 == SDS所保存的字符串的长度
- buf属性 -- 字节数组,用于保存字符串(注:最后一个字节为 \0,遵循C字符串以空字符结尾的惯例,方便直接调用C字符串的库函数)。
2、为什么Redis采用SDS而不是C字符串?
注:C字符串使用长度为N+1的字符数组来表示长度为N的字符串,并且最后一个字符总数空字符串 \0。
Redis使用SDS而不是简单的使用C字符串有如下的好处:
- 常数级的获取字符串的长度。
- 杜绝缓冲区溢出
- 减少修改字符串时带来的内存重分配次数。
- 二进制安全
- 兼容部分C字符串函数
接下就上述好处做具体分析:
1、常数级的获取字符串的长度
因为C字符串本身不记录自身的长度,而需要每次遍历字符串来获取,其时间复杂度为O(n),而SDS只需通过属性len即可以获取,时间复杂度为O(1)。
2、杜绝缓冲区溢出
由于C字符串不记录自身长度带来的另外一个问题就是----容易造成缓冲区溢出(buffer overflow)。
而SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:
当SDS的API对SDS进行修改时,API会先检查SDS的空间是否满足修改需求,如果不满足,API会自动将SDS的空间扩展至执行修改所需大小,然后执行实际的修改操作,所以SDS不需要手动修改SDS的大小,也不会造成缓冲区溢出的问题。
同时,sdscat不仅对SDS进行拼接操作,还会额外分配一定的未使用空间即free的值。
3、减少修改字符串时带来的内存重分配次数
首先我们看一下C字符串的内存重分配策略:
- 增长字符串的操作,比如拼接(append),那么在执行这一步之前,程序需要通过内存重分配来扩展底层数组的大小(注:忘了这一步则会造成 --缓冲区溢出。)
- 缩短字符串的操作,比如截断(trim),那么在执行这一步之前,程序需要通过内存重分配来释放字符串不使用的那部分空间(注:忘了这一步则会造成 --内存泄漏。)
同时,内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以通常比较耗时。这对于经常被用于速度要求严苛、数据修改频繁Redis数据库而言,是无法接受的。
为了避免这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:buf数组长度不一定是字符串数量+1,数组内可以包含未使用的字节 -- free属性记录。
通过未使用空间,SDS实现了 空间预分配 和 惰性空间释放两种优化策略。
-
空间预分配
主要用于优化 SDS的字符串增长操作:当SDS的API需要对SDS进行空间扩展时,程序不仅会为SDS分配修改时所必须的空间,还会分配额外的未使用空间。
- 当SDS的长度 < 1MB时 ---> free = len
- 当SDS的长度 > 1MB时 ---> free = 1MB
通过空间预分配策略:Redis可以减少连续执行字符串增长操作所有的内存重分配次数。
-
惰性空间释放
主要用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不马上使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节记录下来,并等待将来使用。
通过惰性空间释放策略:SDS避免了缩短字符串时所需的内存重分配操作,并未将来可能有的增长操作优化。
同时,SDS也提供了相应的API,让我们可以在有需要时候,真正的释放SDS的未使用空间,所以不用担心会造成内存浪费。
4、二进制安全
C字符串的字符必须符合某种编码(比如ASCII),除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为字符串结尾,这些限制使得C字符串自能保存文本数据,而不能保存图片、音频等二进制文件。
而SDS的buf区不会对数据做任何限制、过滤等操作,写入时什么样,读取时便是怎么样。
这是因为:SDS使用len属性值而不是空字符串\0来判断字符串是否结束。
5、兼容部分C字符串函数
虽然SDS的API是二进制安全的,但是他们一样遵循C字符串以空字符串\0结尾,这使得SDS可以重用一部分<string.h>的函数。
本篇对《Redis设计与实现》一书的总结,感谢大家支持~~~~~~~~~~~~~~~~~~~~