简介:字符串是Redis里最常用到的一种数据类型,Redis的key都是字符串类型的。在Redis中,默认字符串底层实现就是SDS(Simple Dynamic String),即简单动态字符串。
当我们执行命令:
redis> SET name "zhangsan"
其中key的值name
会被作为字符串保存在SDS中,因为value的值zhangsan
在这里也是字符串,也会被保存在SDS中。
对于其他命令,如列表插入操作:
redis> RPUSH book "Java" "Golang" "C#"
键book
和3个值Java
、Golang
、C#
都是以字符串形式保存的,所以底层实现也都是SDS。
因为Redis是用C语言实现的,它的结构体为:
struct sdshdr {
//当前存储字节数组buf已使用的字节数量,不包含终止符'\0'所占的1个长度数量
int len;
//buf数组未使用的字节数量
int free;
//字节数组,用于保存字符串内容
char[] buf;
};
如下图1,字符串内容"Hello Redis"存储在12个长度的buf字符数组里,"Hello Redis"共11个字符(包括中间1个空格符,但不包括最后的终结符\0),所以len=11,由于没有空闲的空间了,所以free=0。
对于下图2,buf数组实际长度为12,存储内容为"Hello",存储字符串所占空间len=5,后面6个空白格代表剩余空闲空间,则free=6。
它们之间的关系可以表示为:free + len + 1 = buf数组分配到的实际长度
,其中1代表额外存储的终结符’\0’。
在看SDS结构体定义的时候有个疑惑:len为什么不是long类型而是int类型呢?不怕int值不够用吗?
首先我们知道,Redis官方指出了字符串最大可容纳长度为512M,即229字节,个人认为Redis采用的C编译器int类型最大值为231-1,所以用int类型存储字符串长度对Redis来说是足够的。这里仅为个人见解,如有错误,请评论区指出~
当SDS的字符串需要增长时,SDS API会先检查未使用空间是否够用,如果足够则直接使用未使用空间,如果当前len+free空间不足以容纳增长后的字符串,Redis需要为SDS分配到能容纳增长后字符串的空间len,除此之外,还会对未使用空间free变量进行额外分配,字符串增长操作是通过Redis自己实现的sdscat函数进行的。
若当前free+len不足以容纳增长后的字符串时,未使用空间的分配策略有如下2种:
若增长后字符串小于1M,则分配的free等于len
(1) 如下图,“Hello"增长为11个字节的"Hello World”,由于当前(free + len = 5) < 11
且11 < 1M
,所以len增长为11,free同样增长为11。
(2)下图同上,(free + len = 6) < 11
且11 < 1M
,len增长为11,free同样增长为11
若增长后的字符串大于等于1M,则分配的free等于1M
(1) 增长前len=1023,增长后大于了1M,free分配为1M,即使从1023增长到10M,free也同样是1M不变,而不是10M。
分配策略源码:
/*
* 最大预分配长度
*/
#define SDS_MAX_PREALLOC (1024*1024)
C语言:在获取字符串长度时需要通过strlen函数或者从首地址开始往后遍历,直到遇到’\0’,时间复杂度为O(n),其中n为字符串有效长度,当值越大时,读取的时间就越长,这对追求高性能的Redis来说无疑是不能接受的,更何况只是为了读取长度而已。
Redis:在读取字符串长度方面,Redis选择了自己维护一个len变量用以保存当前长度,所以读取时间复杂度只需要O(1),当长度改变时只需要同步更新len的值就行,空间换时间,这也是Redis快的一个原因。
C语言:假设有2个字符串s1:"Redis"
和s2:"MongoDB"
,它们的内存地址是紧挨着的,当我们使用C函数strcat为s1拼接上" Cluster"
时,在拼接前如果s1没有提前分配足够的空间,就会导致缓冲区溢出,数据将覆盖到s2的内存。如下图,此时s2是感知不到自身已经被改变了的,读取到的s2居然从MongoDB
变成了Cluster
!
Redis:SDS的空间分配策略杜绝了缓冲区溢出的可能性,每次对字符串修改时都会先对空间进行检查,如果空间不足会提前扩展SDS的空间,保证了不会出现缓冲区溢出的问题。
C语言:C的字符串默认读到\0值就结束,而Redis中的字符串需要存储各种value值,中间是可能出现\0的,如图片、音频、视频文件等部分二进制内容为abcd\0ef
,如果此时使用C字符串存储,读取到的将是abcd
,那么就会出现读取到错误值的问题。
Reids:SDS不会对数据做任何的限制、过滤操作,因为SDS是用len属性判断是否结束,而不是通过结束符\0,所以数据写入时是什么样的,读出来就是什么样的,因此SDS可以保存任意格式的数据。
C语言:每次增长或缩减字符串时,都要对数组进行一次内存重分配。
Redis:
(1) 在增长字符串时,SDS底层调用sdscat函数,会视情况按某种策略提前申请好剩余空间大小,以备下次增长时使用,不会每次都需要申请新的内存空间(空间预分配);
(2) 在缩减字符串时,采用惰性空间释放,SDS底层调用sdsrange或sdstrim等函数,只需将len减小n个长度,free增大n个长度,再改变终结符\0的位置即可。
sdsrange函数源码:
sdstrim函数源码: