Redis中没有直接使用C语言的字符串类进行表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string ,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
在Redis中,C字符串只会作为字符串字面量用在一些字符串不需要修改的地方,比如打印日志那里,如果是需要一个可以修改的字符串的时候,就需要用到SDS表示字符串,比如在Redis中,包含字符串值的键值对在底层都是由SDS实现的。
例如:
SET msg "hello world"
在Redis中执行命令,Redis会在数据库中创建一个新的键值对键值对的键和值都是一个字符串对象,对象底层实现是一个保存这字符串"msg"和"hello world"的SDS
RPUSH fruits "apple" "banana" "cherry"
键值对的键是一个字符串对象,保存着"fruits"的SDS
键值对的值是一个列表对象,列表对象中包含三个字符串对象,这三个字符串对象分别由三个SDS实现,第一个SDS保存"apple"字符串,第二个SDS保存"banana"字符串,第三个SDS保存"cherry"字符串
SDS的功能
1,保存数据库中的字符串
2,SDS用作缓冲区:AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的
SDS的定义
struct sdshdr{ int len;//记录buf数组中已使用的字节的数量,等于SDS所保存字符串的长度 int free;//记录buf数组中未使用字节的数量 char buf[];//字节数组,用于保存字符串 }
1,其中free属性为0,表示SDS没有分配任何未使用的空间
2,len属性5,表示SDS保存了一个5字节长的字符串
3,buf属性是一个char类型的数组,前5个字节保存了redis这5个字符,最后一个保存了空字符'\0'
SDS遵循C字符串一空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性中,遵循空字符结尾这个好处是,SDS可以直接重用一部分C字符串函数库里面的函数
比如有一个指向SDS的指针s,那么我们可以直接用printf("%s",s->buf)来打印redis这个字符串,不需要为SDS编写专门的打印函数
SDS和C字符串的比较
1,常数复杂度获取字符串长度
SDS获取字符串长度,所需复杂度O(1),因为SDS中有len这个属性,可以在SDS的API执行时自动完成,不需要手动修改长度的工作,而C字符串获取长度需要O(N),这样复杂度降低了,获取字符串长度不会成为Redis的性能瓶颈,无论我们对一个非常长的字符串键反复执行
STRLEN
命令也不会对系统性能造成任何影响因为STRLEN
的复杂度为O(1)
2,杜绝缓冲区溢出
C字符串很容易出现缓冲区溢出的问题
比如使用strcat(char *dest,const char *src),把src的字符串拼接到dest上,假设s1="redis",s2="mongodb",假设此时s1有足够的空间来容纳src字符串中所有内容,那么不会出现溢出,一旦不够了,就会出现溢出,如果此时将s1的内容修改为s1="redis cluster",而此刻没有为s1分配足够的空间,那么s1的数据就会溢出到s2所在的空间中
SDS的空间分配策略:当SDS API需要对SDS修改的时候,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展到执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题
SDS API中进行拼接的函数是sdscat,可以将c字符串拼接到给定的SDS所保存的字符串的后面,但是在执行拼接操作之前,sdscat会先检查给定的SDS的空间是否足够,如果不够,sdscat会先扩展SDS的空间,然后执行拼接操作
sdscat(s," cluster")
3,减少修改字符串时带来的内存重分配次数
c字符串的底层是一个n+1的字符长的数组,c字符串的长度和底层数组的长度之间存在这种关联性,每次增长或缩短一个c字符串,都会对这个c字符串的数组进行一次内存重分配的操作
如果增长字符串的操作,那么会先通过内存重分配来扩展底层数组的空间大小,如果没有这个,会产生缓冲区溢出
如果缩短字符串操作,会通过内存重分配来释放字符串不在使用的空间,如果忘了这个会发生内存泄漏
redis作为数据库,会被频繁的修改,这样如果用c字符串会对性能造成影响
为了避免这种问题,SDS通过未使用空间解除字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就是由SDS的free属性记录
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略
1,空间预分配
用于优化SDS的字符串增长的操作:当SDS的API对一个SDS进行修改的时候,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额为的未使用的空间
额外分配的未使用空间数量由以下公式决定:
如果对SDS进行修改之后,SDS的长度将小于1MB,那么程序分配和len属性同样大小的未使用空间
如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间
通过预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数
在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够,API就会直接使用未使用空间,而无须执行内存重分配,通这种策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次
2,惰性空间释放
用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串的时候,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
sdstrim(s,"XY")
//移除SDS字符串中所有的X和Y
通过惰性空间释放策略,SDS避免了缩短字符串字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化
SDS也提供了相应的API,让我们可以在有需要的时候真正的释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费
4,二进制安全
c字符串中字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,这就限制了c字符串只能保存文本数据,不能保存图片,音频,视频,压缩文件这样的二进制数据
sds中不会对其中的数据做任何限制,过滤,或者假设,数据写入什么样,读取就是什么样,redis不是用这个数组来保存字符,而是用他来保存一系列二进制数据,是根据len属性来决定是否结束,而不是空字符
5,兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但是他们一样遵循c字符串以空字符结尾的惯例:这些API会将SDS保存的数据的末尾设置为空字符,并会为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数
<string.h>/strcasecmp(sds->buf,"hello")函数
strcat(c_string,sds->buf);
总结
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N此内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
SDS API
函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建一个包含给定的C字符串的SDS | O(N),N是给定c字符串的长度 |
sdsempty | 创建一个不包含任何内容的空SDS | O(1) |
sdsfree | 释放给定的SDS | O(N),N是被释放SDS的长度 |
sdslen | 返回SDS已使用的空间字节数 | 可以读取len属性获取,O(1) |
sdsavail | 返回SDS的未使用空间字节数 | 可以通过读取free属性,O(1) |
sdsdup | 创建一个给定的SDS副本 | O(N),N是SDS长度 |
sdsclear | 清空SDS保存的字符串内容 | 因为惰性空间释放策略,O(1) |
sdscat | 将给定的c字符串拼接到SDS字符串末尾 | O(N),N是被拼接c字符串的长度 |
sdscatsds | 将给定的sds拼接到另一个sds字符串末尾 | O(N),N是被拼接SDS字符串的长度 |
sdscpy | 将给定的c字符串复制到SDS里面,覆盖SDS原有的字符串 | O(N),N是被复制C字符串的长度 |
sdsgrowzero | 用空字符将sds扩展到给定长度 | O(N),N是扩展新增的字节数 |
sdsrange | 保留SDS给定区间内的数据,不在区间内的 数据会被覆盖或清空 | O(N),N是被保留字段的字节数 |
sdstrim | 接受一个SDS和一个c字符串作为参数,从SDS中移除所有在c字符串中出现过的字符 | O(n^2),n是给定c字符串的长度 |
sdscmp | 对比两个sds字符串是否相同 | O(n),n为两个sds中较短的那个sds的长度 |