Redis
类型中有string
的类型,比如我们用来做缓存的时候,经常会使用到string
类型,这里就主要介绍一个Redis
中string
类型的实现,和C
的字符串有什么区别。
SDS的定义
struct sdshdr {
int len; //记录buf数组中已使用字节的数量,也就是保存字符串的长度
int free; //记录buf数组中未使用字节的数量
char buf[]; //字节数组,用于保存字符串
}
图示:
这个图展示了一个
len
属性为5,free
属性为0的一个字符串。如果对Go
语言了解的话,会联想到Go
中的切片有一个容量和长度的概念。
SDS是遵循C字符串已空字符结尾的惯例,保存空字符的1字节空间不在len
属性中。Redis
会自动完成的。遵循这一惯例,有一个好处就是可以直接重用C字符串函数库里的部分函数。
SDS与C字符串的区别
常数复杂度获取字符串长度
获取一个C字符串的长度,我们必须要遍历整个字符串来计算,这个操作的复杂度为O(n)
。而SDS
中定义了len
的属性,就把获取字符串的长度由O(n)
降到O(1)
。
减小修改字符串时带来的内存重分配次数
对于一个函数N个字符的C字符串来说,底层实现总是一个N+1个字符长的数组(额外的一个用来保存空字符)。因为C字符串和底层数组的长度之间存在着这种关联性,所以对C字符串的修改,总是要对底层数组进行一次内存重分配操作,比如我们要增长字符串,我们需要先对底层数组进行扩容,否则很容易产生缓冲区溢出。又如我们要截断字符串,如果不进行重新分配,则会产生内存泄漏。
为了避免上述的缺陷,SDS通过未使用空间来解除字符串和底层数组长度之间的关联,也就是buf
数组不再是字符的数量 + 1,因为数组中包含了一些未使用的字节,也就是free
属性来记录。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
-
空间预分配 用来优化字符串的增长操作,当需要对SDS进行扩容的时候,程序不仅会为SDS分配必要的空间,还会分配额外的未使用空间。由一下公式决定:
进行修改后,如果长度(len属性的值)小于1MB,那么分配和长度同样大小的未使用空间。如果大于等于1MB,则分配1MB未使用的空间。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。将次数从必定N次降低为最多N次。
-
惰性空间释放 用来优化字符串截断操作,当需要对SDS户必须截断操作是,不会立即回收多出来的字节,而是使用
free
属性将这些字节数量记录下来,等待使用。 通过惰性空间释放策略,可以避免截断字符串时所需的内存重分配操作,并为将来可能的增长操作提供优化。
二进制安全
我们来看一个例子: C:
# include <stdio.h>
# include <strings.h>
int main () {
char test[] = "hello\0y";
printf("test length is %lu", strlen(test)); //test length is 5
}
Redis:
redis> SET mykey "Hello\0y"
"OK"
redis> STRLEN mykey
(integer) 7
redis>
因为C字符串结尾是以\0来认定的,所以如果C字符串中含有这种特殊字符,就会忽略后面的字符。而Redis
都会以处理二进制的方式来处理SDS存放在buf
数组里的数据,不会做任何限制、过滤、或者假设。
兼容部分C字符串函数
前面也提到过,尽管有了len
和free
这两个属性,SDS还是遵循C字符串结尾的管理,并且总会在为buf数组分配空间是多分配一个字节来容纳这个空字符,这样为了可以重用一部分库定义的函数。
总结
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度O(n) |
获取字符串长度的复杂度O(1) |
修改字符串长度N次必然需要执行N此内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h> 库中的函数 |
可以使用一部分<string.h> 库中的函数 |
参考:Redis设计与实现