Redis底层数据结构之简单字符串SDS(二)

496 阅读5分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

Redis版本:3.0.4

SDS的结构定义

Redis 并没有直接使用C语言的字符串来表示Redis的字符串类型,Redis的STRING 类型底层是为 SDS结构的一种类型。也就是C语言常用的结构体。

SDS的结构定义在sds.h/sdshdr结构中

struct sdshdr {
	
	//记录buf数组中未使用字节的数量
	int free;
	
	// 记录buf数组中已使用字节的数量
	// 等于是SDS所保存字符串的长度
	int len;
	
	// 字节数组,用于保存字符串
	char[] buf;
}

==注:在Redis3.2.0版本后,是没有free属性的,而是采用alloc属性来表示buf数组实际分配的长度,也就是free+len的值。==

假设存了值为'Redis'的字符串在Redis,那么它在SDS中的结构大概就是这样子的。

4XpFrn.png

C字符串与SDS的区别

​ 为什么Redis不直接使用C字符串,而是另外封装了SDS这种结构来保存字符串数据呢?这是由于C字符串本身存在几种问题,对于以速度为准的Redis来说并不合适,所以需要封装SDS结构来提升读取速度。以下是他们的几种区别:

1. 常数复杂度获取字符串长度

​ 在C语言中需要获取一个字符串长度的时候,程序需要遍历整个字符串,直到遇见'/0'这个代表字符串结尾的空字符为止,才能计算出字符串的长度,也就是需要O(N)的时间复杂度。

​ 与C语言不同,SDS定义了len属性来表示所保存字符串的长度,只需要使用O(1)的时间复杂度。设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无需额外手动修改len的值。

​ 这样可以确保在获取字符串长度的工作不会成为Redis的性能瓶颈。

2. 杜绝缓冲区溢出

​ C字符串不记录字符串长度还将带来的一个问题是容易造成缓冲区溢出。C语言在修改字符串的时候,需要手动为字符串分配空间,假设忘了执行字符串空间的提前分配,此时空间不足,那么就会导致字符串缓冲区溢出,导致其它字符串的值被修改了。

​ 与C不同,SDS有着自动的空间分配策略。当SDS API需要对SDS进行修改的时候,API会先检查SDS的空间是否足够,如果不够,API会自动将空间扩充到所需要的大小后,再进行字符串的操作修改。

​ 所以使用SDS既不需要手动修改SDS的空间大小,也渡劫了缓冲区溢出的可能。

3. 减少修改字符串时带来的内存重分配次数

​ 内存重分配通常是一个比较耗时的操作,对于Redis来说,数据经常被用在频繁修改且有速度要求的场景。所以每一次的内存重分配可能会对性能造成影响。

​ 通过未使用的空间,SDS实现了**空间预分配 **和 惰性空间释放的两种优化策略。

3.1 空间预分配

​ 空间预分配用于优化字符串增长操作,当SDS的API对SDS进行修改时,如果空间不够,会自动为SDS分配所必须的空间,同时还会为SDS分配额外的未使用空间。

​ 其中额外分配的未使用空间数量由下面公式决定:

  • 如果对SDS修改之后,长度小于1MB,那么程序将为SDS分配与len属性同样大小的未使用空间,也即额外分配free=len的空间长度。
  • 如果长度大于等于1MB,那么程序将额外分配1MB的未使用空间。

也即额外分配的未使用空间小于等于1MB。

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

3.2 惰性空间释放

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

​ 通过惰性空间释放策略,SDS避免了缩短字符串时所需的空间重分配操作,并为将来可能有的增长操作提供了优化。

​ SDS有相应的API提供来真正释放SDS的未使用空间。

4. 二进制安全

​ C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串结尾的空字符之外,字符串不能含有空字符,否则最先被程序读入的空字符将被误认为是字符串的结尾,这就限定了C字符串只能保存文本数据,不能保存图片、音频视频等二进制数据。

​ 采用SDS结构就可以避免这个问题,程序不会读到空字符就认为是字符串的结尾,而是以len长度决定。

5. 兼容部分C字符串的函数

​ 在SDS的结构中,char数组里保存的字符数据在最后都会以'/0'作为末尾。这种好处就是程序可以直接使用到C字符串的函数,而无需另外封装了。

总结

C字符串与SDS的区别如下:

C 字符串SDS
获取字符串长度的时间复杂度为O(N)O(1)
API是不安全的,可能会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度必然执行N次的内存重分配小于等于N次的内存重分配
只能保存文本数据可以保存一系列的二进制数据,如文本、图片、音频/视频
可以使用<string.h>的全部函数只能使用<string.h>的部分函数