通俗易懂的redis数据结构之SDS
redis数据结构-SDS(Simple Dynamic String)
redis是我们常用的缓存之一,但是仅仅限于会用,已经在市场上淘汰了,我们必须去了解他的底层来让我们更深刻的去使用它。
SDS定义
每个sds.h/sdshdr(至于这是什么意思想必我就不多介绍了吧,不清楚的可自行查阅)结构表示一个SDS的值:
struct sdshdr {
// 记录的buf数组中已经使用的字节数量== sds保存的字节的长度
int len;
// 记录的buf数组中未使用的字节数量
int free;
// 自己数组,用于保存字符串
char buf[];
}
这里我可以给你们画一个类似的结构,比如我在buf数组里保存了一个字符串叫sjt,结构如下(自己随便画的,功底不行,意思是这个意思):
最后一个字节是空字符,是不是很好奇啊,是因为SDS遵循了C字符串以空字符结尾的惯例,这个空字符是不计算在字符串长度之内的,这样的好处就是可以直接重用C字符串函数库里面的函数。
举个例子吧,比如有一个指向我上图中SDS的指针s,就可以直接使用<stdio.h>/printf函数,通过执行下列语句
printf("%s",s->sjt)//直接打印出保存的字符串值sjt,不用SDS编写专门的打印函数
下面正是我们关心的,估计大家都想到了,那为什么不直接去使用C字符串呢,非得搞什么SDS。
SDS和C字符串的区别
计算机行业学过C语言的我们都知道,C语言是使用长度为N+1的字符数组来保存长度是N的字符串,字符数组的最后一个元素总是空字符’\0’。
其实使用C串的话不能满足redis这种高效,安全的功能方面的需求,主要有以下几点:
1. O(1)复杂度获取字符串长度
C字符串并不记录自身的长度信息,想要获取字符串长度的话,必须去遍历整个字符串,对每个字符进行计数,知道遇到结尾的那个空字符串位置,这个复杂度显然是O(n),这肯定人家redis作者的一个优化点。
2. 杜绝缓冲区溢出
C字符串是不保存长度的,当执行stract函数去拼接字符的时候,假定用户在拼接这个函数时,已经为字符串分配了足够多的内存,拼接的时候一旦假设不成立,就会产生缓冲区溢出。然而当内存中有紧邻着的C字符串S1和S2,对S1进行拼接的时候,会覆盖了S2保存的内容。
SDS它的空间分配策略杜绝了这种情况发生的可能性,SDS API会检查空间是否足够,不够就会去扩展,扩展了才去执行修改。
3. 减少修改字符串带来的内存重分配次数
C字符串是不保存长度的,每次修改的时候,都会去判断空间大小是否足够,如果是append操作,不去判断这步的话,会造成缓冲区溢出;如果是trim截取操作,还得需要内存重分配去释放掉不再使用的空间,忘了的话,有可能会出现内存泄漏(这个太重要了,占着茅坑不拉屎!!!)。
SDS有未使用空间free,就可以实现了空间预分配和惰性释放两种优化策略。
空间预分配:SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,程序不仅会对SDS分配修改所必须的空间,还会为SDS分配额外未使用的空间。
对SDS修改以后,SDS的长度小于1MB,那么程序会分配和len属性同样大小的空间,这时即len=free。SDS的buf数组的实际长度就是len+free+1字节。
对SDS修改以后,SDS的长度大于等于1MB,比如是20MB,那么程序会分配1MB,这时即free=1MB。SDS的buf数组的实际长度就是20MB+1MB+1byte。
通过空间预分配可以减少每次去内存重新分配次数,内存重新分配涉及很复杂的算法,并且也可能需要执行系统调用,所以比较耗时。
惰性空间释放:当SDS的API需要缩短SDS保存的字符串时,程序并不会立即使用内存重分配来回收缩短后多出来的字节,而是加到了free里。你可能会问,那这些不使用的空间会造成内存浪费吗?这是不会的,因为SDS也提供了相应的API会在我们有需要的时候,真正的释放SDS的未使用空间,大可不必担心。
4. 二进制安全问题
C字符串的字符必须符合某种编码(比如ASCII),并且除了字符串末尾之外,字符串里面不能包含空字符串,不然最先被程序读入到的空字符串会被误认为是字符串末尾,这些限制就会使C字符串只能保存文本数据,不能保存图片、音频、视频等这样的二进制数据。比如"hello sjt",C字符串就只能读出hello字符串,忽略了sjt。SDS是二进制安全的,你的程序写入的什么,读出来的就是什么,不会出现C这样的情况,因为SDS它使用的len属性来判断的字符串的值,而不是空字符串。
5. 兼容部分C字符串函数
为什么这么说呢?是因为buf数组最后一个字节不是空字符嘛,这也是为了让安歇保存文本数据的SDS可以重用一部分<string.h>库定义的函数。这个库里函数就比较多了,比如strcasecmp函数使用它来对比SDS保存的字符串和另一个C字符串:
strcasecmp(sds->buf,"hello world");// redis就不用自己写专门的函数了
和这个类似的比如strcat函数,可以将一个保存文本数据的SDS作为第二个参数,将SDS字符串追加到C字符串的后面:
strcat(c_string,sds->buf); //同样,redis也不用写专门的函数了
这里基本上就是我对SDS的了解了,欢迎补充和指点。既然我们没有天生的撸码天赋,那我们就花时间去磨碎各个知识点。接下来会陆续把双端队列,压缩链表,hash,数组,跳表补充。加油!!!