Redis 数据结构字符串底层剖析(简单动态字符串)

引言

Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示.

在Redis 里面,C字符串只会作为字符串面量(string literal)用在一些无须对字符串值进行修改的地方。

在一个可以被修改的字符串值里面,Redis就会使用SDS来表示字符串值,比如:Redis数据库里面,包含字符串值得健值对在底层得实现都是由SDS实现的以及AOF的缓冲区等(注:在一些无需更改的地方字符串是用C字符串)

基本结构

 

Redis 数据结构字符串底层剖析(简单动态字符串)

????思考:

(1)为什么不直接使用C字符串,而要自己实现一个SDS呢?

肯定是C字符串整体上满足不了需求嘛,因为C语言使用的简单的字符串表示方式,并不能满足Redis对字符串在安全性、效率性以及功能方面的要求

(2)SDS遵循了C字符串以空格符结尾的惯例,为什么要这样设计呢?

好处就是SDS可以直接重用一部分C字符串函数,不用自己去实现

 

获取字符串长度

C字符串不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,知道遇到代表字符串结尾的空字符为止,时间复杂度为O(N)

Redis中的SDS因为记录的字符串长度,所以获取长度信息 时间复杂度为O(1),因此,在反复获取长度的时候能大大减少性能消耗

缓存区溢出问题

C语言不记录自身长度还可能造成缓冲区溢出(buffer overflow)

但SDS的空间分配策略考虑到这点,当SDS的API需要对SDS进行操作修改时候,API会先检查SDS的空间是否满足修改需求,如果不满足的话,API会自动将SDS的空间扩展至需要执行修改所需的大小,然后才执行相关操作

????思考

每次重新分配空间都只是按需分配,那和C语言每次重新分配新空间进行存储没多大区别呀?性能好像也没增加多少呀?

所以如何减少空间重新分配也在Redis考虑之中

空间预分配策略

空间预分配就是用与优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改并需要对SDS进行空间扩展的时候,程序不仅仅会为SDS分配修改所必须的空间,还会为SDS分配额外的使用空间

额外空间分配数量公式:

如果SDS进行修改之后

(1)SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时len属性的值江河free属性的值相同

(2)SDS的长度大于等于1MB,那么程序将会分配1MB的未使用空间

这样将N次字符串所需内存重分配次数从必须N次变为最多N次(注:会用空间浪费,典型的空间换时间策略)

惰性空间释放(有额外分配空间,那回收怎么办?)

惰性空间释放用于优化SDS的字符串缩短操作,当SDS的API需要缩短保存的字符串,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量保存起来,并等待将来使用

当然SDS也提供了相应的API,让我们可以在需要的时候,真正释放SDS的使用空间,所以不用担心惰性空间释放策略造成的内存浪费

二进制安全

C中字符串只能保存文本数据,不能保存图片、音频等这样的二进制数据

Redis的SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制方式来处理SDS存放在buf数组里面的数据,不进行任何限制、过滤或者假设(这也是为什么buf属性称为字节数组的原因)

因此Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据

Redis 数据结构字符串底层剖析(简单动态字符串)

 

总结:

Redis只会使用C字符串作为字面量,大多数情况下,Redis使用SDS(Simple Dynamic String,简单动态字符串)作为字符串表示

优点:

(1)获取字符串长度时间复杂度降为O(1)

(2)减少了缓存区溢出

(3)额外空间预分配策略减少了内存重分配次数

(4)二进制安全

(5)兼容部分C语言字符串函数