我们知道Redis中有五种数据结构:String、Map、List、Set、ZSet,那么这些数据结构在Redis中又是如何实现的呢?
String(动态字符串)
-
介绍SDS:
-
SDS是什么?
- 在Redis中并没有直接使用C语言的字符串(以空字符结尾的字符数组),而是构建了一种名为简单动态字符串(SDS)的抽象类型,并且SDS用作Redis的默认字符串。
- 在Redis中一个可以被修改的字符串值都是用SDS来表示这样一个字符串值的,因此在Redis中键值对的底层都是由SDS来实现的。
-
举例:如果客户端执行命令:
SET msg "hello world"
,那么Redis将在数据库中创建一个新的键值对,其中 -
键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串"msg"的SDS
-
键值对的值也是一个字符串对象,对象的底层实现是保存着字符串"hello world"的SDS。
-
对于SDS不仅用来保存Redis中的字符串值,还被用作缓冲区(buffer):AOF模块中的AOF缓冲区
、以及客户端状态中的输入缓冲区。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
例子如下图:
上图中定义了一个Redis
字符串,该字符串存放的情况如上图中。
-
free属性为0,表示这个SDS没有分配任何未使用空间
-
len属性值为5,表示这个SDS保存了一个5字节长的字符串
-
buf属性是一个char类型的数组,该数组中依次存放Redis五个字符,而最后一个字节则保存空字符
'/0'
SDS遵循C语言字符串以空字符结尾
的惯例,保存空字符的1字节空间不计算在SDS的len属性中。并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作都是由SDS函数自动完成。这样的一个好处是可以重用C语言字符串函数库中的函数。
以上的SDS中并没有分配未使用空间
上图中在Redis字符串后面新增了五个空字节,这个五个字节表示分配的未使用空间。因此free=5
-
分析属性
len
- 在SDS中len属性记录SDS的长度,所以获取一个SDS长度的
复杂度仅为O(1)
,这确保获取字符串长度的工作不会成为Redis的性能瓶颈。
设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无需手动修改长度。
- 在SDS中len属性记录SDS的长度,所以获取一个SDS长度的
-
杜绝缓冲区溢出
在平时初始化数组时一般需要给数组初始化一定的长度,那么在SDS中是如何保证buf
(char数组)的内存时足够的呢?并且保证buf不会内存溢出的呢?
在SDS中设定了一套空间分配策略,该策略完全杜绝了发生缓冲区溢出的可能性:
当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需要的要求,如果不满足的话,API会自动讲SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,如果满足则直接修改。因此使用SDS不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。
例如:我们使用SDS Api中的拼接字符串函数sdscat拼接msg字符串,那么执行sdscat(msg,'Cluster')
此时SDS在执行sdscat(msg,'Cluster')
之前会检查msg的长度是否足够,如果msg目前的空间不足以拼接Cluster,那么sdscat会先扩展msg的空间,然后再去执行sdscat(msg,'Cluster')
,拼接完成后如图
sdscat操作不仅对SDS进行拼接操作,还会为SDS分配13字节的未使用空间(free=13)
,拼接后的字符串长度是13,这就涉及到了SDS的空间预分配策略
。
空间预分配策略主要用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,此时会为SDS分配额外的未使用空间。
空间预分配策略如下:
-
如果对SDS修改后,SDS的长度(len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时len属性的值和free属性的值相同。也就是如上的msg例子中修改后msg的
len=13
,free=13
,buf的实际长度为13+13+1=27
-
如果对SDS修改后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。例如,如果修改后SDS的len变为20MB,那么程序会分配1MB的未使用空间。SDS的buf数组的实际长度为20MB+1MB+1byte。
通过空间预分配策略Redis可以减少连续执行字符串增长操作所需的内存重分配次数。 -
惰性空间释放
惰性空间释放主要用于优化SDS的字符串缩短操作,当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待以后使用。
例如通过SDS API中的sdstrim函数来移除指定的字符,当执行sdstrim(msg,"Cluster")
后,SDS并不会释放Cluster的7字节空间,而是将这7个字节保留在SDS中,如果以后对SDS进行增长操作,则这些空间就会排上用场。
在SDS的buf数组中保存的并不是字符,而是保存的一系列的二进制数据,这样就确保了Redis可以适用各种不同的场景。