本文转载自微信公众号《学Java的小姐姐》,作者0618,学Java的小姐姐。转载本文请联系正在学习Java的小姐姐公众号。前言Redis是用C写的,C中没有string、list、hash、set、zset等数据类型,那么C是如何实现这些数据类型的呢?我们将从本文源码开始分析??。API使用我们的文章来学习string的底层实现。先看API的简单应用,将str1变量设置为helloworld,然后使用debug对象+变量名看一下。注意标红的代码是embstr。如果我们把str2设置为helloworldhelloworldhelloworldhelloworldhell,字符长度为44,然后用debug对象+变量名来看,注意红色标记的代码是embstr。但是当我们设置为helloworldhelloworldhelloworldhelloworldhello时,字符长度为45,然后使用debug对象+变量名来看,注意红色标记的代码是raw。最后我们把str3设置为整数100,然后用debug对象+变量名来看一下。请注意,红色标记的代码是int。所以Redis的string类型有三种存储方式。当字符串长度小于等于44时,底层使用embstr;当字符串长度大于44时,底层使用raw;当设置为整数时,底层使用int。embstr和raw的区别所有类型数据结构的最外层都是RedisObject,这部分会说,先有个大概的了解,因为本文的重点不在这里。如果字符串小于等于44,则实际数据和RedisObject在内存中是相邻的,如下图。如果字符串大于44,则实际数据与RedisObject在内存中的地址不相邻,如下图。还是那句话,这些都不重要,后面再说,现在提一下,只是为了让大家对Redis的String类型有一个大概的了解,从整体把握入手。我们今天要说的其实是实际的数据,也就是上图中指针指向的位置??。SDSHdr的定义其实并不是直接存储数据,而是进行了封装。看下面的代码,可以看到分为五种,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。sdshdr5和其他四个的区别很明显,sdshrd5其实更节省内存空间。其他四个乍一看都差不多,包括使用长度len,总长度alloc,flags(感觉没用,有知道的朋友欢迎指教),实际数据buf。//定义五个不同的结构,sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64struct__attribute__((__packed__))sdshdr5{unsignedcharflags;//8-bitflagcharbuf[];//pointertoactualdata};struct__attribute__((__packed__))sdshdr8{uint8_tlen;/*使用长度*/uint8_talloc;/*总长度*/unsignedcharflags;charbuf[];};struct__attribute__((__packed__))sdshdr16{uint16_tlen;uint16_talloc;unsignedcharflags;charbuf[];};struct__attribute__((__packed__)))sdshdr32{uint32_tlen;uint32_talloc;unsignedcharflags;charbuf[];};struct__attribute__((__packed__))sdshdr64{uint64_tlen;uint64_talloc;unsignedcharflags;charbuf[];};SDS具体逻辑图假设我们设置一个字符串是hello,那么他的SDS的availablelengthlen是8,usedlengthlen是6,如下图。注意:Redis会根据具体的字符长度选择对应的sdshdr,但是每种类型都差不多,所以简单画了下图。SDS的优势我们可以看到就是对字符数组进行了重新包装,但是为什么呢,直接使用字符数组不是更简单吗?这要从C语言和Java语言的根本区别说起。更快速的获取字符串的长度我们都知道Java字符串提供了length方法,lists提供了size方法。我们可以直接获取尺寸。但是C就不一样了,它更偏向于底层实现,所以没有直接的使用方法。这就带来了一个问题。如果我们想得到一个数组的长度,只能从头开始遍历。当我们遇到第一个'\0'时,表示数组结束。这个速度太慢了,不可能每次都改变可变数组,因为需要获取长度。因此设计了SDS数据结构,在原来的字符数组外加上总长度和已用长度,这样每次都可以直接得到已用长度。复杂度为O(1)。数据是安全的,不会被截断。如果使用传统的字符串来保存图片、视频等二进制文件,中间可能会出现'\0'。如果按照原来的逻辑,就会导致数据丢失。所以可以用usedlength来表示字符数组是否已经结束。SDS关键代码解析获取常用值(抽象常用方法)一些常用方法写在sds.h中,比如计算sds的长度(即sdshdr的len),计算sds的自由长度(即sdshdr可用长度alloc-already使用长度len),计算sds可用长度(即sdshdr的alloc)等。但是你有没有疑惑,这不就是一行代码的事情吗?为什么要抽象方法?那么问题来了,我们把sdshdr分为五种,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。那么在我们实际使用的时候,我们要区分当前是哪种类型,取其对应的字段或者设置对应的字段。//计算sds对应的字符串长度,其实就是获取字符串对应的sdshdr的len值-1];//flags和上面定义的宏变量7做位运算lencaseSDS_TYPE_16://2returnSDS_HDR(16,s)->lenofsdshdr8;caseSDS_TYPE_32://3returnSDS_HDR(32,s)->len;caseSDS_TYPE_64://5returnSDS_HDR(64,s)->len;}return0;}//计算sds对应的free长度,其实就是alloc-lenstaticinlinesize_tsdsavail(constsdss){unsignedcharflags=s[-1];switch(flags&SDS_TYPE_MASK){caseSDS_TYPE_5:{return0;}caseSDS_TYPE_8:{SDS_HDR_VAR(8,s);returnsh->alloc-sh->len;}caseSDS_TYPE_16:{SDS_HDR_VAR(16,s);returnsh->alloc-sh->len;}caseSDS_TYPE_32:{SDS_HDR_VAR(32,s);returnsh->alloc-sh->len;}caseSDS_TYPE_64:{SDS_HDR_VAR(64,s);returnsh->alloc-sh->len;}}return0;}//设置lenstaticinlinevoidsdssetlen(sdss,size_tnewlen){unsignedcharflags=s[-1];开关(标志&SDS_TYPE_MASK){caseSDS_TYPE_5:{unsignedchar*fp=((unsignedchar*)s)-1;*fp=SDS_TYPE_5|(newlen<len=newlen;break;caseSDS_TYPE_16:SDS_HDR(16,s)->len=newlen;break;caseSDS_TYPE_32:SDS_HDR(32,s)->len=newlen;break;caseSDS_TYPE_64:SDS_HDR(64,s)->len=newlen;break;}}//给sdshdr的len添加多少大小staticinlinevoidsdssinclen(sdss,size_tinc){unsignedcharflags=s[-1];switch(flags&SDS_TYPE_MASK){caseSDS_TYPE_5:{unsignedchar*fp=((unsignedchar*)s)-1;unsignedcharnewlen=SDS_NTYPE_5(标志)+inc;*fp=SDS_TYPE_5|(newlen<len+=inc;break;caseSDS_TYPE_16:SDS_HDR(16,s)->len+=inc;break;caseSDS_TYPE_32:SDS_HDR(32,s)->len+=inc;break;caseSDS_TYPE_64:SDS_HDR(64,s)->len+=inc;break;}}//获取sdshdr的总长度staticinlinesize_tsdsalloc(constsdss){unsignedcharflags=s[-1];switch(flags&SDS_TYPE_MASK){caseSDS_TYPE_5:returnSDS_TYPE_5_LEN(flags);caseSDS_TYPE_8:returnSDS_HDR(8,s)->alloc;caseSDS_TYPE_16:returnSDS_HDR(16,s)->alloc;caseSDS_TYPE_32:returnSDS_HDR(32,s)->alloc;caseSDS_TYPE_64:returnSDS_HDR(64,s)->alloc;}return0;}//设置sdshdr的总长度staticinlinevoidsdssetalloc(sdss,size_tnewlen){unsignedcharflags=s[-1];switch(flags&SDS_TYPE_MASK){caseSDS_TYPE_5:/*Nothingtodo,thistypehasnototalallocationinfo.*/break;caseSDS_TYPE_8:SDS_HDR(8,s)=newlen;中断;caseSDS_TYPE_16:SDS_HDR(16,s)->alloc=newlen;中断;caseSDS_TYPE_32:SDS_HDR(32,s)->alloc=newlen;中断;caseSDS_TYPE_64:SDS_HDR(64,s)->alloc=newlen;中断;}}创建一个对象我们通过sdsnew方法创建一个对象,可见通过判断init是否为空来确定初始大小,然后调用方法sdsnew(这里方法名相同,只是参数不同,是方法的重载),先根据长度判断类型(上面说了五种,不记得可以往上翻),然后分配对应的根据类型分配内存资源,最后加上C语言的结束符'\0'sdssdsnew(constchar*init){size_tinitlen=(init==NULL)?0:strlen(init);returnsdsnewlen(init,initlen);}sdssdsnewlen(constvoid*init,size_tinitlen){void*sh;sdss;chartype=sdsReqType(initlen);//根据长度判断类型/*空字符串,使用sdshdr8,这里是经验写法,当你想构造一个空字符串放一个长度超过32的字符串时*/if(type==SDS_TYPE_5&&initlen==0)type=SDS_TYPE_8;inthdrlen=sdsHdrSize(type);//到下一个方法,它们已经放在一起了unsignedchar*fp;/*flagspointer.*///分配内存sh=s_malloc(hdrlen+initlen+1);if(!init)memset(sh,0,hdrlen+initlen+1);if(sh==NULL)returnNULL;s=(char*)sh+hdrlen;fp=((unsignedchar*)s)-1;//根据不同的类型创建不同的结构体,调用SDS_HDR_VAR函数//为不同的结构体赋值,比如使用长度len,总长度allocswitch(type){caseSDS_TYPE_5:{*fp=type|(initlen<len=initlen;sh->alloc=initlen;*fp=type;break;}caseSDS_TYPE_16:{SDS_HDR_VAR(16,s);sh->len=initlen;sh->alloc=initlen;*fp=type;break;}caseSDS_TYPE_32:{SDS_HDR_VAR(32,s);sh->len=initlen;sh->alloc=initlen;*fp=type;休息;}caseSDS_TYPE_64:{SDS_HDR_VAR(64,s);sh->len=initlen;sh->alloc=initlen;*fp=type;break;}}if(initlen&&init)memcpy(s,init,initlen);//附加'\0's[initlen]='\0';returns;}//根据实际字符长度判断类型staticinlinecharsdsReqType(size_tstring_size){if(string_size<1<<5)returnSDS_TYPE_5;if(string_size<1<<8)returnSDS_TYPE_8;if(string_size<1<<16)returnSDS_TYPE_16;#if(LONG_MAX==LLONG_MAX)if(string_size<1ll<<32)returnSDS_TYPE_32;#endifreturnSDS_TYPE_64;}删除String类型不是直接回收内存,而是修改字符使其为空字符,这其实就是Lazyrelease,等以后调用sdsempty方法的时候再调用上面的sdsnewlen方法。/*将sds字符串修改为空(零长度)。*但是,不会丢弃所有已经存在的buffer,而是设置为freespace*这样,下一次append操作就不需要分配到*当SDS保存的string要截短时,程序不会立即使用内存fullyallocate将缩短后的多余字节回收,以备后用。*/voidsdsclear(sdss){sdssetlen(s,0);s[0]='\0';}sdssdsempty(void){returnsdsnewlen("",0);}添加字符(展开)强调!!!添加字符string,sdscat的入参是sds和stringt,先调用sdsMakeRoomFor扩展方法,然后追加一个新的字符串,最后加上结束符'\0'。让我们看看扩展方法是如何实现的。第一步是调用常用方法中的sdsavail方法,获取还剩多少空闲空间。如果空闲空间大于要添加的字符串t的长度,不展开直接返回。如果没有足够的可用空间,您想扩展容量。第二步是确定您要扩展多少。这里有不同的情况。如果当前字符串小于1M,那么直接将容量翻倍。如果当前字符串大于1M,直接加1M。三是判断加入字符串后的数据类型是否和原来的一样。如果是一样的就好了。如果没有,您想要创建一个新的sdshdr并将所有现有数据移动过来。这个是不是有点抽象?比如str的当前字符串是hello,当前是sdshdr8,总长度为50,已用6,空闲44。现在如果要添加一个长度为50的字符t,首先要看是否需要扩容。50明显大于44,需要扩容。第二步扩容多少?str的长度小于1M,所以扩容了一倍,新的长度为50*2=100。第三步,50+50对应的sdshdr类型还是sdshdr8吗?明明还是sdshdr8,所以不用迁移数据,在原来的基础上加t就可以了。sdssdscat(sdss,constchar*t){returnsdscatlen(s,t,strlen(t));}sdssdscatlen(sdss,constvoid*t,size_tlen){//在sds.h中调用sdslen,即取使用的长度size_tcurlen=sdslen(s);//扩展方法s=sdsMakeRoomFor(s,len);if(s==NULL)returnNULL;memcpy(s+curlen,t,len);sdssetlen(s,curlen+len);s[curlen+len]='\0';returns;}sdssdsMakeRoomFor(sdss,size_taddlen){void*sh,*newsh;//调用sds.h获取自由长度allocsize_tavail=sdsavail(s);size_tlen,newlen;chartype,oldtype=s[-1]&SDS_TYPE_MASK;inthdrlen;//自由长度大于需要增加的,不用扩充,直接返回if(avail>=addlen)returns;//在sds.h中调用sdslen,即取可用长度len=sdslen(s);sh=(char*)s-sdsHdrSize(oldtype);//len加上要加的sizenewlen=(len+addlen);//#defineSDS_MAX_PREALLOC(1024*1024)//当新长度小于1024*1024时,直接容量翻倍if(newlen