今天我们来探讨一下redis在实现字符串对象,列表对象,哈希对象,集合对象,有序集合对象时,底层相关的数据结构是咋样设计的
一、简单动态字符串
总所周知redis是使用c 语言编写的内存数据库,但是在实现字符串对象时并没有直接使用c语言传统的字符串,而是自己构建了一种名为简单动态字符串的抽象类型(sds)。sds结构在redis中使用非常广泛,当redis需要的不是一个简单的字符串字面量而是可以被修改的字符串值时使用的都是sds结构。
在代码中这样表示
len 字段记录已使用的字节数量,free字段记录数组中未使用的字段,和一个字节数组
在图中我们看见了在数组的末尾有添加一个 '\0' 空字符,这个空字符不会被记录到使用空间中,在写入sds结构中时API自动帮我们添加上的。
为什么要这样做呢?
因为当我们在字符末尾添加上这个空字符之后,我们就可以直接套用部分c字符串函数库中实现的函数而不需要自己再重新实现
设计这样的结构体似乎比普通的字符串对象所需要的空间更大,为什么要这样设计呢?
正是因为redis作为一个主打速度的内存数据库,在空间占用和速度的选择时,选择了牺牲空间来换取时间的一种策略,多添加的len字段,在需要获取字符长度时能在o(1)复杂度的时间范围内获取结果,对比c语言则需要从头开始遍历直到遇见空字符才停下来,快了不少。
free字段,则记录当前字节数组中还有多少的空闲大小,当要放入的字符串的长度小于free大小时则可以直接放入,不需要重新分配内存。
杜绝内存溢出
在c语言中我们要在一个字符串后面拼接另一个字符串时,因为我们并不知道被拼接的字符串是多大的大小,所以会导致拼接之后导致缓冲区溢出的现象发生,在使用sds结构体之后,在拼接字符串时,api会进行字符串长度和sds中的free字段进行比较,如果free字段大于想要拼接的字符串长度,则可以直接拼接在后面,如果free字段小于想要拼接的字段长度,则会进行分配空间操作以保证空间足够放下。
预分配空间与惰性空间
当空闲空间不够拼接字符串的长度时,进行预分配空间操作,当所用空间小于1MB时,那么redis 会为字符串数组分配len属性相同大小的空闲空间为下一次字符串操作留下空间,当所用空间大于1MB时,redis会为字符串分配1MB的空闲空间留给下次拼接操作。正是因为有这样的预分配以及留下的惰性空间才让redis杜绝内存溢出和内存分配操作次数从必定n次降到最多n次,大大提升运行速度。
惰性空间的释放
当sds中存储的字符串长度缩小时,redis并不会立即回收空闲内存,而是留着等待下一次的操作使用,只是将len属性减小至目前的字符串长度,于此同时,redis也提供了相应的api在有需要时真正的回收内存。
二进制安全
我们知道c语言中的字符串结构是依靠空字符 '\0' 来判断是否结束,这就表明出来末尾之外其他任何地方都不能出现这个空字符,否则就会被认为是字符串的末尾,用来存储字符串还行,但是用来存储图片、音频、视频、二进制文件的话就不安全了
但是使用sds结构的redis则没有这样的担心,因为redis用来判断是否结束是依靠len属性来判断,就算中间出现空字符也不会被截断。
总结
1、通过len属性可以常数级别获取到存储字符串的长度(c语言需要遍历)
2、通过free字段常数级别获取空闲空间大小
3、预分配与惰性空间将必然n次分配空间操作降到最多n次内存分配操作,杜绝内存溢出
4、二进制安全,通过len属性判断字符串有没有结束而不是空字符
5、可以兼容部分c语言字符串库函数