Redis没有直接使用c语言传统的字符串标识(以空字符串结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
1 SDS的定义
上图展示了SDS的结构:
- free表示未使用空间,上图的属性值为5,表示这个SDS分配的未使用空间为5字节长
- len表示字符串的长度,注意不包含尾部SDS默认的空字符串,上图表示该字符串的长度为5字节
- buf属性是一个char类型的数组,数组的前五个字节分别保存了R、e、d、i、s五个字符串,而最后一个字符串则保存了空字符串\0.
SDS遵循c字符串以空字符串结尾的惯例,这一好处是SDS可以直接重用一部分c字符串函数库里面的函数
2.SDS与c字符串的区别
上图展示了一个简单的c语言字符串。c语言使用的这种简单的字符串表示方式,并不能满足Redis对字符串在安全性、效率以及功能方面的要求,下面将对比c字符串和SDS之间的区别,并说明SDS比c字符串更适用于Redis的原因。
2.1常数复杂度获取字符串长度
c字符串并不记录自身的长度信息,所以为了获取一个c字符串的长度,程序必须要遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符串为止,整个操作的复杂度为O(N)。上图获取字符串长度的遍历次数为5
对于SDS来说,由于其结构存储了len属性,所以只需要通过len即可直到Redis字符串的长度,每次的时间复杂度都为O(1)
总结:通过使用SDS,Redis将获取字符串长度的复杂度控制在O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。
2.2杜绝缓冲区溢出
除了获取字符串长度的复杂度高之外,c字符串不记录自身长度带来的另外一个问题是会带来缓冲区溢出
举个例子:程序里有两个在内存中紧邻的c字符串s1和s2,如图2-7所示
如果一个程序员决定通过执行strcat(s1, "Cluster")将s1的内容修改为"Redis Cluster",但他粗心地忘了在执行前为s1分配足够的空间,那么执行之后,s1的数据将溢出到s2所在的空间,导致s2保存的内容被意外地修改,如图2-8所示。
与c不同,SDS的内存分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间拓展至执行修改锁需的大小,然后再执行实际的修改操作。
SDS也需要做上面的操作。如图2-9,执行strcat之前先检查len,发现内存空间不足以拼接"Cluster",SDS会拓展内存空间,再执行后面的strcat操作,执行之后的SDS如图2-10所示
注意:执行之后SDS的free变成了13,这并非bug,后面会讲到
2.3减少修改字符串时带来的内存重分配次数
由于c字符串未记录自身的长度,所以对于一个包含N个字符的c字符串来说,这个c字符串的底层实现总是一个N+1的字符数组。每次增长或者缩短一个c字符串,程序都总要保存这个c字符串的数组进行一次内存重分配操作:
- 如果增长字符串,那么在执行这个操作之前,程序需要先通过内存重分配来拓展底层数组的空间大小 -------- 如果忘了这一操作,将会产生缓冲区溢出。
- 如果缩短操作,比如截断操作,程序需要通过内存重分配来释放字符串不再使用的那部分空间 --------- 如果忘了这一操作,将会产生内存泄露。
为了避免c字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就又SDS的free属性记录。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
2.3.1空间预分配
空间预分配用户优化SDS的字符串操作:当SDS的API对一个SDS进行修改,并且需要对SDS空间进行拓展时,程序不仅会对SDS分配修改所需要的空间,还会为SDS分配额外的未使用空间。其中额外的未使用空间数量由一下公式决定:
- 如果修改之后的长度小于1MB,那么程序分配和len一样的未使用空间。比如修改之后的SDS实际长度为13,那么也会分配13字节的未使用空间,SDS的buf数组的实际长度会变成13+13+1=27字节
- 如果修改时候大于1MB,那么程序会额外为SDS分配1MB的未使用空间
2.3.2惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用
3.二进制安全
c字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符串,否则最先被程序读入的空字符串会被误认为是字符串结尾,这限制使得c字符串只能保存文本数据,不能保存想图片、音频、视频这样的二进制数据。比如保存"Redis Cluster",c字符串会认为Redis后面的空字符串是结尾,使得Cluster被截掉。
而SDS有len属性,使得SDS并不需要判断空字符串来确认是否为末尾。这样的结构保证了SDS的二进制是安全的,可以保存任意结构的二进制数据。
4、兼容部分c字符串函数
由于SDS默认会为buf分配一字节的空间来保存空字符串结尾,这使得SDS可以重用部分的c字符串函数,避免了不必要的代码重复