Redis源码解析-SDS

对于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),它的结构如下:

Redis源码解析-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结构时,不要使用内存对齐的方式,而是采用紧凑的方式分配内存。因为在默认情况下,编译器会按照内存对齐的方式,给变量分配内存。

Redis源码解析-SDS

上一篇:AutoCAD图纸中有标注文字看不见什么原因?如何解决?


下一篇:strace 跟踪系统调用