《redis设计与实现》-- 第二章知识内容
redis中涉及到的两种字符串
c语言传统字符串
即以空格结尾的字符数组,简称c字符串。
SDS(simple dynamic string)——简单动态字符串
SDS是redis的默认字符串表示。
redis也会用到c字符串,但是只会作为字符串字面量,用在一些不用修改的字符串上面。
eg:
redisLog(REDIS_WARING,"Redis is now ready to exit, bye bye ...");
这里,“ …”中的内容即是字符串字面量。当redis需要的不仅仅是字符串字面量时,就会使用SDS来表示字符串值。
redis中的键值对
eg:redis> SET msg "hello world" OK
在例子中键值对中的键说的就是 msg,它是一个字符串对象,其底层实现是一个保存着"msg"字符串的SDS。
同理,“hello world”即为值,也是字符串对象,底层是保存着“hello world”字符串的SDS。
当键值对中值有多个时,此时的值就是一个列表对象,而在列表对象中包含了多个字符串对象,其中每个字符串对象都由一个SDS来实现。
关于什么是字符串对象,什么是列表对象,现在不做分析。
SDS的主要用途
1.保存数据库中的字符串值
2.被用作缓冲区(buffer):AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区。
SDS的定义
sds.h/sdshdr 结构表示一个SDS值:
struct sdshdr
{
//记录buf数组中已经使用的字节数
//即为SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,保存字符串
char buf[];
}
free值为0,表示SDS没有未分配的空间
len值为5,表示SDS保存了一个五字节长度的字符串
buf是char类型数组,默认在字符串结尾添加’\0’表示结束。
c语言以空字符串结尾,保存空字符’\0’的1字节空间并不会被计算到字符串长度里面,它是被额外分配的1字节空间,是自动分配的。SDS遵循此规则。
遵循此规则的好处
可以直接重用一部分c字符串函数库中的函数。
eg:定义一个指针s指向sdshdr,sdshdr *s
。此时调用c库中<stdio.h>/printf
函数,即:
printf("%s",s->buf);
可以直接调用printf函数来打印输出,无需再为SDS编写打印函数。
下面展示一个SDS分配了五字节未使用空间的结构。
结构很容易理解。
SDS与C字符串的区别
C字符串
c字符串使用N+1的字符数组来表示长度为N的字符串,并在最后一个元素总是空字符’\0’。
SDS
SDS以复杂度O(1)来获取字符串长度
显然SDS不仅可以保存字符串,而且可以记录字符串长度,以及buf剩余的未使用空间长度。
而c字符串要想获取字符串长度,就需要从头到尾进行遍历,就复杂度上SDS为O(1)<C为O(n)
SDS的长度工作是SDS的API执行时,自动完成的。这样,SDS将获取字符串长度从O(n)降低到O(1),确保了获取字符串长度不会影响redis的性能。
SDS杜绝缓冲区溢出
C字符串容易造成缓冲区溢出(buffer overflow)。
如<string.h>/strcat
。该函数是将src中的字符串内容拼接到dest字符串的末尾:
char *strcat(char *dest,const char *src);
上文中讲到,C字符串并不会记录长度,所以strcat这个函数会假定用户在调用此函数时,用户以及为dest分配了足够的内存空间,但事实上,一旦为dest分配的空间不足以容纳src中的所有内容,就会产生缓冲区溢出。
例:现有两个字符串s1和s2如下图。
执行 strcat(s1,“Cluster”);
此时s1中的内容就会溢出到s2的位置上,而s2的内容会因此被间接地修改。如图所示
SDS的空间分配策略可以完全杜绝发生缓冲区溢出的可能性:
SDSAPI要对SDS进行修改时,API执行步骤。
1.检查SDS的空间大小是否满足修改所需的要求。
2.若不满足则API自动将SDS扩展至所需大小
3.执行API实际的修改操作
所以SDS不需手动修改空间大小,也无需担心缓冲区溢出问题。
以sdscat(s,“Cluster”);为例。(该函数是SDS的拼接函数对应strcat函数)
现在执行函数
可以看到,sdscat函数会先检查空间是否足够,在发现不足以拼接"Cluster"时,先拓展了空间,然后执行拼接操作。
拼接后有一个有趣的现象:此时空间多出了13字节的未使用的内存空间,这又与什么有关呢?