1 前言
在开始学习Redis时,除了知道Redis的用处之外,对于Redis的底层数据结构进行深入了解,显得尤为重要。
如果不了解底层数据结构,也将错失体验Redis高性能的美妙之处。
2 什么是SDS
SDS即简单动态字符串(Simple Dynamic String),是redis自定义的,用于存储字符串的数据结构。这里需要补充说明,C中的字符串只是以字面量的形式出现在redis的使用业务场景之中
3 SDS数据结构定义
为了对SDS有更进一步的了解,还需要对其数据结构进行了解。查看Redis源码可知,SDS的数据结构的设计非常巧妙,具体如下
SDS的数据结构中有三个属性:
1 len :表示buf中已经占用的空间,也就是SDS中实际占用了的字符串的长度
2 free:表示buf中剩余可用空间的长度
3 buf[] : 表示实际字符串存储的空间
举个例子:
a. 当free=0时,表示当前sds的buf[]没有任何未使用的数据空间
b. free = 5 ,len = 5,表示带有5个未使用空间的SDS
从上述数据结构可知,相对于C语言中String,SDS多了2个属性,len和free,用来标记buf[]中已用和尚未使用的空间长度。
Redis作为一个Key-Value型的数据库,在读写方面性能有极高的要求。这样设置,使得Redis读取字符串长度所需要的时间复杂度从O(N)降低为O(1),同时也就完美解决了读取字符串长度时,时间复杂度为O(n)而引起的性能瓶颈。
因为C语言在读取字符串长度时,计算过程如下
而Redis则只需要直接读取len即可。
注: SDS采用与C语言String一样的字符串结尾方式“\0” 这样的好处是能够复用一部分C语言自带的String函数
4 为什么要自定义SDS
首先,先明确一点,Redis是用C语言进行编写,而C语言已经自带了字符串这一数据结构,那么既然已经自带了字符串,为何Redis还需要重新定义SDS?
理由如下:
1 常数复杂度获取字符串长度: 从Redis的SDS数据结构上看,当客户端尝试获取某个字符串长度时,只需要开销O(1)的常数时间复杂度即可完成。而C语言中的String,在性能上存在诸多瓶颈,只能一个个去遍历计数,时间复杂度为O(N)。
2 杜绝缓冲区溢出:C语言中的String,在对字符串进行写操作时,会存在缓冲区溢出。而Redis的SDS则不存在这样的问题,具体的原因,还是和SDS的数据结构有关。
由于SDS记录了字符串空间(buf[])的使用情况,因此当SDS API需要对SDS进行修改时,首先会检查SDS的空间是否满足所需的要求,如果不满足,则API会自动对SDS进行扩容,扩容至执行修改所需要的大小,而在C语言中,则需要手动扩容,极易发生缓冲区溢出的情况。
3 二进制安全:C语言中的String,由于严格按照ASCII码的编码规范,所以只能读写文本类型数据,而无法存储图片等二进制文件,而这在Redis场景中将受到很大限制
4 减少修改字符串长度时所需的内存重新分配:C语言中的String,在进行内存重分配时,性能较低,因为没有采用空间预分配策略,每次进行字符串拼接操作时,一旦空间不足,必然执行空间重分配操作,很消耗性能。(Redis采用空间预分配和惰性空间释放策略,使得空间分配从最少N次,变为最多N次)
对比C中的字符串,当C字符串需要增长或缩短时,程序总需要对C字符串的数组进行内存重新分配的动作,即假如有N次对C字符串的写操作,那么至少需要N次对内存进行重分配动作。而主要的问题在于:内存分配是一个比较耗时的操作,其中涉及较为复杂的算法并可能需要执行系统调用(用户态和系统态的切换等),操作一旦频繁,则会对性能产生严重影响。
基于以上情况,SDS实现了空间预分配和惰性空间释放两种优化策略。
1 空间预分配
空间预分配用于优化SDS的字符串增长操作:当SDS API对一个SDS进行修改,并需要对SDS进行扩容时,程序会同时为SDS分配额外的未使用空间。
具体规则如下:
当len < 1MB 时,程序会为当前SDS分配和len一样大小的free。
例如:经过修改后,len = 45字节,则SDS的buf[]数组实际长度为 len + free + 1 = 91字节(多出的1字节,是'\0',用于字符串结束标志,这样设计的目的是为了复用部分的C字符串API)
当len > 1MB时,程序会为当前SDS分配固定1MB长度的free
例如:经过修改后,len = 30MB,则SDS的buf[] 实际长度 = len + free + 1 byte 即 30MB + 1MB + 1byte
Redis采用空间预分配的策略,使得在增加SDS操作的场景下,修改SDS的次数从原先C字符串的最少N次变为最多N次
2 惰性空间释放
有对SDS增长操作的优化策略,那必然也会有对SDS缩短操作的策略,而惰性空间释放,就是Redis所采用的策略。
原理很简单,即当去除SDS中的部分字符时,多余的空间不会被立马回收,而是会进行保留。与此同时,SDS也提供了相应的API,在有需要时,真正释放SDS未使用的空间,所以不用担心内存浪费。
5 兼容部分C字符串函数。
SDS的API都是二进制安全的,但SDS同样遵循C字符串以空字符结尾的惯例,并总会在为buf[]数组分配空间时,多分配一个字节来容纳空字符,以便重用<string.h>库中定义的函数,从而避免不必要的代码重复。