对于Redis来说,键值对中的键是字符串,值有时也是字符串。我们在Redis中写入一条用户信息,记录了用户姓名、性别、所在城市等,这些都是字符串,如下所示:
SET user:id:100{"name":"zhangsan","gender":"M","city":"beijing"}
在Redis的字符串结构需要满足以下条件
- 能支持丰富且高效的字符串操作,比如字符串追加、拷贝、比较、获取长度等;
- 能保存任意的二进制数据,比如图片等;
- 能尽可能地节省内存开销。
说到字符串会立即想到C语言中的char*字符数组,它可以通过string.h中定义的诸如字符串比较函数strcmp、字符串长度计算函数strlen、字符串追加函数strcat等来进行操作。
strcat源码如下:
char *strcat(char*dest,const char*src){
//将目标字符串复制给tmp变量
char *tmp=dest;
//用一个while循环遍历目标字符串,直到遇到“\0”跳出循环,指向目标字符串的末尾 while(*dest)
dest++;
//将源字符串中的每个字符逐一赋值到目标字符串中,直到遇到结束字符 while((*dest++=*Src++)!=‘\o‘)
return tmp;
}
综上,char*字符数组有以下不足
- 操作效率低:获取长度需遍历,O(N)复杂度
- 二进制不安全:无法存储包含 \0 的数据(\0代表字符串结束)
SDS
结构
简单动态字符串(Simple Dynamic String,SDS),它的结构如下:
SDS创建函数sdsnewlen
typedef char *sds; //sds是char*的别名
sds sdsnewlen(const void*init,size_t initlen){
void*sh; //指向SDS结构体的指针
sds s; //sds类型变量,即char*字符数组
...
sh=s_malloc(hdrLen+initlen+1); //新建SDs结构,并分配内存空间
...
s=(char*)sh+hdrLen;//sds类型变量指向SDS结构体中的buf数组,sh指向SD
...
if(initlen&&init)
memcpy(s,init,initlen); //将要传入的字符串拷贝给sds变量s s[initlen]=‘\0‘; //变量s未尾增加\0,表示字符串结束
returns;
}
SDS字符串追加函数sdscatlen()
sds sdscatlen(sds s,const void*t,size_t len){
//获取目标字符串s的当前长度
size_t curlen=sdslen(s);
//根据要追加的长度Len和目标字符串s的现有长度,判断是否要增加新的空间 s=sdsMakeRoomFor(s,len);
if(s==NULL) return NULL;
//将源字符串t中1en长度的数据拷贝到目标字符串结尾
memcpy(s+curlen,t,len);
//设置目标字符串的最新长度:拷贝前长度curLen加上拷贝长度
sdssetlen(s,curlen+Len);
//拷贝后,在目标字符串结尾加上\0
s[curlen+1en]=‘\0‘;
returns;
}
SDS通过记录字符数组的使用长度和分配空间大小,避免了对字符串的遍历操作,降低了操作开销,进一步就可以帮助诸多字符串操作更加高效地完成,比如创建、追加、复制、比较等。
封装
SDS把目标字符串的空间检查和扩容封装在了sdsMakeRoomFor
函数中,并且在涉及字符串空间变化的操作中,如追加、复制等,会直接调用该函数。这一设计实现,就避免了开发人员因忘记给目标字符串扩容,而导致操作失败的情况。比如,在使用函数 strcpy(char *dest,const char *src)时,如果src的长度大于dest的长度,代码中也没有做检查的话,就会造成内存溢出。
紧凑型字符串结构
SDS结构中有一个元数据flags,表示的是SDS类型。事实上,SDS一共设计了5种类型,分别是sdshdr5(已经不再使用)、sdshdr8、sdshdr16、sdshdr32和sdshdr64。这5种类型的主要区别就在于,它们数据结构中的字符数组现有长度len和分配空间长度alloc,这两个元数据的数据类型不同。
sdshdr8:
struct__attribute__((__packed__))sdshdr8{
uint8_t len;/*字符数组现有长度*/
uint8_t alloc;/*字符数组的已分配空间,不包括结构体和\0结束字符*/
unsigned char flags;/*SDS类型*/
char buf[];/*字符数组*/
};
现有长度len和已分配空间alloc的数据类型都是uint8t。uint8t是8位无符号整型,会占用1字节的内存空间。当字符串类型是sdshdr8时,它能表示的字符数组长度(包括数组最后一位\0)不会超过256字节(2的8次方等于256)。而对于sdshdr16、sdshdr32、sdshdr64三种类型来说,它们的len和alloc 数据类型分别是uint16t、uint32t、uint64t即它们能表示的字符数组长度,分别不超过2的16次方、32次方和64次方。这两个元数据占用的内存空间在sdshdr16、sdshdr32、sdshdr64类型中,则分别是2字节、4字节和8字节。
SDS之设计不同的结构头(即不同类型),是为了能灵活保存不同大小的字符串,从而有效节省内存空间。因为在保存不同大小的字符串时,结构头占用的内存空间也不一样,这样一来,在保存小字符串时,结构头占用空间也比较少。
代码中的__attribute__((__packed__))
的作用就是告诉编译器,在编译 sdshdr8结构时,不要使用内存对齐的方式,而是采用紧凑的方式分配内存。因为在默认情况下,编译器会按照内存对齐的方式,给变量分配内存。