跟随大斌阅读源码-Redis7-对象编码的简单动态字符串简单动态字符串的抽象类型,使用SDS作为Redis默认的字符串表示。在Redis中,C字符串只是在一些不需要修改字符串的地方作为字符串字面量使用,比如打印日志:serverLog(LL_WARNING,"SIGTERMreceivedbuterrorstryingshutserver,查看日志获取更多信息");当Redis需要的不仅仅是一个字符串字面量,而是一个可以修改的字符串值时,Redis会适配SDS来表示字符串。比如在数据库中,包含字符串值的键值对都是底层由SDS实现的。或者以简单的SET命令为例,执行如下命令redis>SETmsg"helloworld"ok,那么Redis会在data中新建一个键值对,其中:键值对的key是一个字符串和对象的底层实现是一个包含字符串“msg”的SDS。键值对的值也是一个字符串对象,对象的底层实现是一个保存字符串“helloworld”的SDS。除了在数据库中用来保存字符串值外,SDS还用作缓冲区。AOF模块中的AOF缓冲区,以及client态中的输入缓冲区,都是由SDS实现的。接下来,让我们详细了解一下SDS。1SDS的定义在sds.h中,我们会看到如下结构:typedefchar*sds;我们可以看到SDS相当于char类型。这是因为SDS需要兼容传统的C字符串存储,所以它的类型设置为char。但需要注意的是,SDS并不等同于char*,它还包括一个header结构,一共有5种headers,源码如下:struct__attribute__((__packed__))sdshdr5{//deprecatedunsignedchar旗帜;/*3lsboftype,and5msbofstringlength*/charbuf[];};struct__attribute__((__packed__))sdshdr8{//长度小于2^8的字符串类型uint8_tlen;//SDS中存储的字符Stringlengthuint8_talloc;//SDSunsignedcharflags分配的长度;//标志位,占用1个字节,使用低3位存储SDS的类型,高5位不用charbuf[];//存储的真实字符字符串数据};struct__attribute__((__packed__))sdshdr16{//字符串类型uint16_tlen长度小于2^16;/*使用*/uint16_talloc;/*不包括标头和空终止符*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};struct__attribute__((__packed__))sdshdr32{//字符串类型uint32_tlen长度小于2^32;/*使用*/uint32_talloc;/*不包括标头和空终止符*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};struct__attribute__((__packed__))sdshdr64{//字符串类型uint64_tlen长度小于2^64;/*使用*/uint64_talloc;/*不包括标头和空终止符*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};之所以有5种header,是为了让不同长度的字符串使用相应大小的header,提高内存利用率。一个完整的SDS结构由相邻的内存地址组成。它由两部分组成:header:包括字符串的长度(len)、最大容量(alloc)和flags(不包括sdshdr5)。buf[]:字符串数组。这个数组的长度等于最大容量加1,存放的是真正的字符串数据。SDS的一个例子如图1-1所示:例子中各字段说明如下:alloca:SDS分配的空间大小。图中显示分配的空间大小为10。len:SDS保存字符串大小。如图所示,保存了一个5字节的字符串。buf[]:这个数组的长度等于最大容量加1,存放的是真正的字符串数据。图中表示数字的前5个字节分别存放了'H'、'e'、'l'、'l'、'o'这五个字符,而最后一个字节存放的是空串'0'。SDS遵循C字符串以null结尾的约定,容纳空字符的大小不计入SDS的len属性。另外,在字符串末尾添加空字符串等操作都是由SDS函数(sds.c文件中的相关函数)自动完成的。此外,遵循空终止约定,也可以直接重用C字符串库中的部分函数。比如我们可以直接使用printf()函数打印s->buf:printf("%s",s->buf);这样,我们就可以直接使用C函数打印字符串“Redis”,而不用写SDSCode打印函数的转换。2SDS相对于C字符串有什么优势在C语言中,用长度为N+1的字符数组来表示长度为N的字符串,字符数组的最后一个元素永远为空字符“0”。C语言使用的字符串表示不能满足Redis对字符串重安全性、效率和功能性的要求。因此Redis设计了SDS来满足自身的相关需求。接下来,我们将从以下几个方面来了解SDS相对于C字符串的优势:获取字符串的长度;缓冲区溢出;修改字符串时内存重新分配的次数;二进制安全;字符串不记录自己的长度信息,所以在C语言中,为了获取C字符串的长度,程序必须遍历整个字符串,直到遇到代表字符串结束的空字符。这个操作的复杂度是O(N)。对于Redis,一旦遇到很长的字符串,使用STRLEN命令很容易影响系统性能。与C字符串不同的是,由于SDS在len属性中记录了SDS保存的字符串长度,所以获取一个SDS长度的复杂度仅为O(1)。而且,设置和更新SDS长度的工作在执行时由SDSAPI自动完成,因此使用SDS时无需手动修改长度。通过使用SDS,Redis将获取字符串长度的复杂度从O(N)降低到O(1),保证了获取字符串长度的工作不会成为Redis的性能瓶颈。2.2消除缓冲区溢出C字符串没有记录自己的长度,这不仅使得获取字符串的长度更加复杂,而且容易造成缓冲区溢出。C语言中的strcat()函数可以将src字符串的内容拼接到dest字符串的末尾:char*strcat(char*dest,constchar*src);因为C字符串没有记录自己的长度,strcat函数在执行时,假设用户已经为dest分配了足够的内存来容纳src字符串的所有内容。而一旦这个假设不成立,就会发生缓冲区溢出。例如,假设程序中有两个在内存中相邻的C字符串s1和s2,其中s1存储字符串“redis”,s2存储字符串“mysql”。存储结构如图2-1示例:如果我们执行如下语句:strcat(s1,"666");修改s1的内容为“redis666”,但是在执行strcat()之前没有给s1分配足够的空间,然后执行strcat()之后,s1的数据会被移到s2所在的空间,导致内容保存在s2中被不小心修改,如图2-2所示:与C字符串不同,SDS的空间分配策略完全杜绝了buffer溢出的可能:当SDSAPI需要修改SDS时,API会先检查SDS的空间是否满足修改的要求,如果不满足,API会自动将SDS的空间扩展到修改需要的大小,然后进行实际的修改操作,所以使用SDS不需要手动修改SDS空间大小,也不会出现前面提到的缓冲区溢出问题。2.3减少内存重新分配的次数由于C字符串的长度slen和底层数组的长度salen总是有如下关系:salen=slen+1;//1是空字符的长度,所以每增加或缩短一个C字符字符串,总会对C字符串数组进行一次内存重分配操作:增长字符串。程序需要通过内存重新分配来扩展底层数组空间的大小。如果错过这一步,可能会发生缓冲区溢出。缩短字符串。程序需要通过内存重新分配释放底层数组未使用的空间。如果错过这一步,可能会发生内存泄漏。然而,内存重新分配涉及复杂的算法并且可能需要执行系统调用,因此内存重新分配是一个耗时的过程。对于Redis来说,所有的耗时操作都要进行优化。基于此,SDS通过空间预分配和惰性空间释放对字符串增长和缩短操作进行了优化。2.3.1空间预分配空间预分配是指:当需要扩展SDS的空间时,程序不仅分配必要的空间,还为SDS额外分配未使用的空间。关于SDS的空间扩展,源码如下:#sds.c/sdsMakeRoomFor()...newlen=(len+addlen);//SDS的最新长度if(newlen
