当前位置: 首页 > Linux

Redis源码学习(2)-Redis中的动态字符串实现(上)

时间:2023-04-06 05:18:07 Linux

Redis源码学习(二)——Redis中的动态字符串实现(上)数据,无需关心扩容问题。Redis使用typedefchar*sds;来描述这个动态String,它在内存中的分布格式是一个StringHeader和StringHeader后面连续的动态内存,而sds指向StringHeader后面连续内存的第一个字节。它在内存中的分布可以如下图所示:Strings的头信息和sds的头信息主要包括sds分配的缓存大小和使用的缓存大小。根据需要分配的缓存大小,在Redis中定义了五种类型的sds头信息:sdshdr5,定义了类型SDS_TYPE_5sdshdr8,定义了类型SDS_TYPE_8sdshdr16,定义了类型SDS_TYPE_16sdshdr32,定义了类型SDS_TYPE_32sdshdr64,定义了类型SDS_TYPE_64定义。的大小,其中sdshdr5表示最大可以分配1<<5的缓存大小,sdshdr8表示最大可以分配1<<8的缓存大小。除sdshdr5外,其余头信息按如下格式定义(以sdshdr32为例):struct__attribute__((__packed__))sdshdr32{uint32_tlen;//使用缓存的长度uint32_talloc;//包含header分配的缓存总长度和末尾的空终止符unsignedcharflags;//低三位存放header类型信息,SDS_TYPE_32charbuf[];//动态分配的缓存};sdshdr5header的格式为//低三位保存header类型信息,高五位用于表示使用缓存的长度charbuf[];//动态分配缓存};根据我们需要的String的长度,选择不同的sdshdr,可以达到节省空间的目的。通过Redis中sdshdr数据类型的定义,我们可以发现,无论是哪种sdshdr,sdshdr.buf缓存字段前面都有sdshdr.flags标志字段。在Redis中,我们实际使用的sds变量实际上是指向sdshdr.buf的指针,而整个SDS就是一个连续的内存分配,那么,如果我们通过sds向前移动一个字节长度,那么sds[-1]一定是sdshdr。此SDS数据的标志字段。通过位操作,我们可以知道SDS数据使用的sdshdr的类型,然后通过指针偏移,我们可以得到整个SDS的头部信息,后续对SDS的基本操作都是通过这种方式实现的。Strings常见的底层操作在头文件中定义了几个宏和静态函数来实现对sds的基本操作。#defineSDS_HDR(T,s)((structsdshdr##T*)((s)-(sizeof(structsdshdr##T))))给定一个sds数据,使用SDS_HDR获取其对应的sdshdr指针的头部.使用方法是通过SDS数据获取对应的sdshdr.flags,然后获取HeaderType,通过调用SDS_HDR获取整个header信息,例如:unsignedcharflags=s[-1];switch(flags*SDS_TYPE_MASK){...caseSDS_TYPE_8:SDS_HDR(8,s)->len=new_len;休息;...}#defineSDS_HDR_VAR(T,s)structsdshdr##T*sh=(void*)((s)-(sizeof(structsdshdr##T)));给定一个sds数据,声明一个sdshdr指针变量sh,将sds对应的sdshdr头赋值给sh变量。静态内联size_tsdslen(constsdss);给定一个sds数据,获取其使用的缓存的长度。具体实现方法是:结合sdshdr的定义和sds在内存中的分布结构,通过s[-1]来获取StringHeader中的flags数据。根据flags,计算出它对应的是什么类型的shshdr。调用宏SDS_HDR获取对应的StringHeader指针,进而获取len字段数据。静态内联size_tsdsalloc(constsdss);给定一个sds数据,获取其分配的缓存sdshdr.buf的总长度。静态内联size_tsdsavail(constsdss);给定一个sds数据,获取其缓存可用的长度,可以理解为sdsavail(s)==sdsalloc(s)-sdslen(s)。staticinlinevoidsdssetlen(sdss,size_tnewlen);给定一个sds数据和一个新的长度newlen,将sds头部的len字段设置为newlen。staticinlinevoidsdsinclen(sdss,size_tinc);给定一个sds数据和一个需要增加的长度inc,将sds头部的len字段增加inc。需要注意的是,在sdsinclen中,并没有检查增加的长度inc是否合法,只是将inc添加到sdshdr.len中。这就需要调用者在调用sdsinclen接口之前检查长度是否合法。静态内联voidsdssetalloc(constsdss,size_tnewlen);给定一个sds数据和一个新的长度newlen,将sds头部的alloc字段设置为newlen。通过源码我们可以发现,SDS_TYPE_5类型的sds数据无论是sdshdr结构,还是基本操作接口的处理,都与其他类型的sds数据有很大区别。Redis在2015年7月15日的提交中引入了这个新的sds类型,作者自己给出的提交信息是:引入了一个新的类型SDS_TYPE_5,头部只有一个字节,只有字符串长度,没有可用的额外长度信息在字符串的末尾。结合后续其他操作接口对SDS_TYPE_5类型的处理,我们可以认为该类型的sds数据主要用于存储不超过32字节的长度,不会重新分配缓存数据。对此,Redis的作者也给出了一个建议:如果要重新分配字符串,就不要使用TYPE5,因为没有freespaceleftfield很糟糕。构造和释放Strings的操作函数staticinlineintsdsHdrSzie(chartype);静态内联字符sdsReqType(size_tstring_size);上面定义在src/sds.c头文件中的两个静态函数是用来返回特定HeaderType的头结构的长度,头结构的长度根据一个string_size的长度,返回一个合适的HeaderType。sdssdsnewlen(constvoid*init,size_tinitlen);sdssdsempty(void);sdssdsnew(constchar*init);sdssdsdup(constsdss);voidsdsfree(sdss);sdsnewlen函数是这个系列的基础该函数的主要目的是在给定初始化内存的头指针init和初始长度initlen的情况下构造一个sds数据。这个函数会使用sdsReqType接口根据你需要初始化的数据的长度initlen来选择使用的HeaderType,8位,16位,32位或者64位,使用s_malloc调用它分配一个cache长度为headerSize+initlen+1,之所以需要多分配一个字节的cache是??因为redis中的sds总是以0作为结束标志,因为需要多分配一个字节的cache给这个结束标记。同时,由于sds本质上是二进制安全的,也就意味着0可能会出现在数据的中间,所以这就是为什么我们需要头信息结构中的sdshdr.len字段。同时初始化Header中的type、len、alloc字段,并调用memcpy将init指向的数据复制到sds的buffer中,并以0作为结束标志(null-terminated)。以下三个接口都是通过调用sdsnewlen来完成相关功能的:sdsempty函数用于创建一个空的sds数据。sdsnew函数可以从空终止的C风格字符串创建sds数据。请注意,此接口不是二进制安全的,因为它在内部使用strlen来计算传入数据的长度。sdsdup函数可以通过一个给定的sds数据复制一个新的sds数据,并返回上一个接口。sdsfree函数通过调用s_free接口释放给定的sds数据。需要注意的是,释放的内容包括sdsheader指针,以及它前面的Header数据的整个缓存。用于调整Strings长度信息的操作函数voidsdsupdatelen(sdss);通过对内部数据调用strlen来更新sds的长度,这个接口在手动重写sds缓存时很有用。一般来说,该接口用于缩短sds的sdshdr.len字段,但该接口不会修改sdshdr.buf中的数据。无效sdsclear(sdss);用于清除一个sds数据的内容,类似于sdsupdatelen接口,该函数不会释放或修改已有的缓存。它只是将sdshdr.len长度字段清除为零,但空间仍然存在。sdssdsMakeRoomFor(sdss,size_taddlen);sdsMakeRoomFor用于扩展给定sds数据的可用缓存空间,保证用户在调用该接口后可以继续往缓存中写入addlen字节的内容,但是这个操作不会改变已经使用的缓存大小,也就是说,它不会改变sdslen调用的结果。一些细节:如果sds当前的可用空间,即sdsavail的大小大于addlen,那么这个函数将不会执行任何操作。同时,为了减少重复分配缓存带来的系统开销,sdsMakeRoomFor接口会一直分配一些预留的(最多1MB)缓存:newlen=(len+addlen)if(newlen