Redis源码分析(五)——简单动态字符串(sds)
2014-10-18 21:34
746 查看
Sds(Simple Dynamic String)是Redis底层所使用的字符串表示,它被用在几乎所有的Redis模块中。
Redis是一个键值对数据库(K-V DB),数据库的值可以是字符串、集合、列表等多种对象,而键则只能是字符串对象。 由于char*类型的功能单一,抽象层次低,不能高效的支持一些Redis常用的操作(比如对字符串的追加操作和长度计算操作,而在Redis中对字符串的追加以及计算长度都是很常见的操纵,每次对char*字符串进行追加都必然导致内存的重新分配(realloc)这将严重影响性能),另外Reids的字符串表示还应该是二进制安全的。
所以在Redis的内部绝大部分使用的都是sds而不是char*来表示字符串。
二进制安全字符串:二进制安全是一种主要用于字符串操作函数相关的计算机编程术语。其本质就是将字符串中的所有字符都看作 原始的、无任何特殊格式意义的数据流。将所有字符视为按照一串0和1的形式编码的二进制数据,而不会对'\0'
,'\n' 等“特殊”的字符赋予不同的特殊含义。
Sds字符串总是以\0作为结束(sds对应的sdshdr的len不包括末尾的\0),由于sdshdr保存了字符串的长度len,因此在sds的中间可以包括\0字符(二进制安全的)。
在对字符串sds的操作时,总是把目标的C字符串cher*(sds)转换为 sdshdr(相当于string对象)进行操(及时更新sdshdr的已有字符数len,剩余空间free和实际存放字符串的buf等成员参数),然后再返回更新后的sds。 这样就使得C的字符串cher*具有了类似高级string的各种操作方法,从而能够方便的对C字符串sds计算长度以及追加等复杂操作。
Rdis通过对sds封装为sdshdr结构体,为sds字符串的操作提供了大量的操作方法,使得在Rdis中方便高效的使用字符串。
以下对sds的主要API函数进行分析,具体见注释。
sds.c /sds.h :
sds.c:
扩容函数:(增加buf的free空间):如果原来free足够用则直接返回。否则,如果buf->len+addlen 小于SDS_MAX_PREALLOC(1M),则buf总空间扩为len+addlen的两倍;否则buf总空间扩为len+SDS_MAX_PREALLOC的两倍。 这也是sds由于char*的主要原因之一。在sds追加内容的时候,按需要触发扩容操作,并预留free的剩余空间,这样在下次追加时当free足够大时就可以不用再次重新分配空间(realloc),从而提高性能。以空间换时间,类似于动态数组vector。
对于预留的空间,可用 sdsRemoveFreeSpace(sds s)函数释放归还。然而free剩余空间的释放不是自动进行的,这可以通过设置系统,比如让系统定时自动释放掉free的剩余空间。
sdscpylen:将给定C串复制到指定sds(覆盖原有内容),需要时扩容,t为二进制安全的字符串,因此需要传入其长度,不能通过strlen函数计算(strlen函数计算长度时遇到\0则结束),如果给定的字符串为非二进制安全的,则不需要传入len参数。
把数值转换为字符串存放到s指定位置 的辅助函数。 s指向的字符串长度至少等于SDS_LLSTR_SIZE,返回转换所得的以\0结尾的字符串的长度
sds sdscatfmt(sds s, char const *fmt, ...):字符串格式化输出,输入原字符串,格式,参数
字符串格式化输出到sds,输入原字符串,格式,各需要格式化到sds的参数 */
例: sds x="--";
x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN,LLONG_MAX);
memcmp(x,"--Hello Hi! World -9223372036854775808,9223372036854775807--",60) == 0)
小结:
对比C字符串,sds有以下特性:
* 可以高效的执行长度计算
* 可以高效的执行追加操作
* 二进制安全
sds会为追加操作进行优化: 加快追加操作的速度,并降低内存分配次数,代价是多占用了内存空间(buf的free预留空间),而且这些内存不会被主动释放。
Redis是一个键值对数据库(K-V DB),数据库的值可以是字符串、集合、列表等多种对象,而键则只能是字符串对象。 由于char*类型的功能单一,抽象层次低,不能高效的支持一些Redis常用的操作(比如对字符串的追加操作和长度计算操作,而在Redis中对字符串的追加以及计算长度都是很常见的操纵,每次对char*字符串进行追加都必然导致内存的重新分配(realloc)这将严重影响性能),另外Reids的字符串表示还应该是二进制安全的。
所以在Redis的内部绝大部分使用的都是sds而不是char*来表示字符串。
二进制安全字符串:二进制安全是一种主要用于字符串操作函数相关的计算机编程术语。其本质就是将字符串中的所有字符都看作 原始的、无任何特殊格式意义的数据流。将所有字符视为按照一串0和1的形式编码的二进制数据,而不会对'\0'
,'\n' 等“特殊”的字符赋予不同的特殊含义。
Sds字符串总是以\0作为结束(sds对应的sdshdr的len不包括末尾的\0),由于sdshdr保存了字符串的长度len,因此在sds的中间可以包括\0字符(二进制安全的)。
在对字符串sds的操作时,总是把目标的C字符串cher*(sds)转换为 sdshdr(相当于string对象)进行操(及时更新sdshdr的已有字符数len,剩余空间free和实际存放字符串的buf等成员参数),然后再返回更新后的sds。 这样就使得C的字符串cher*具有了类似高级string的各种操作方法,从而能够方便的对C字符串sds计算长度以及追加等复杂操作。
Rdis通过对sds封装为sdshdr结构体,为sds字符串的操作提供了大量的操作方法,使得在Rdis中方便高效的使用字符串。
以下对sds的主要API函数进行分析,具体见注释。
sds.c /sds.h :
<span style="font-size:18px;">typedef char *sds; //sds是C char*的别名 //动态字符串结构体 struct sdshdr { unsigned int len;//当前buf已占用空间 unsigned int free;//buf剩余空间 char buf[];//实际保存字符串数据的地方 }; //计算sds的长度,返回的size_t类型的数值 static __inline size_t sdslen(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->len; } //计算sds剩余可用长度 static __inline size_t sdsavail(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->free; } //创建一个指定长度的sds,接受一个C字符串作为初始值 sds sdsnewlen(const void *init, size_t initlen); //创建一个sds,初始化为给定C字符串 sds sdsnew(const char *init); //创建一个只包含空白串""的sds sds sdsempty(void); //获取sds的长度 size_t sdslen(const sds s); //复制给定sds sds sdsdup(const sds s); //释放给定sds void sdsfree(sds s); //获取给定sds剩余可用空间 size_t sdsavail(const sds s); //将给定sds的buf扩张至指定长度,无内容的部分用\0填充 sds sdsgrowzero(sds s, size_t len); //将给定sds扩展至给定长度,并将一个给定C字符串追加到sds末尾 sds sdscatlen(sds s, const void *t, size_t len); //将给定C字符串追加到给定sds末尾 sds sdscat(sds s, const char *t); //将一个给定sds追加到另一个sds末尾 sds sdscatsds(sds s, const sds t); //将一个给定C字符串的len部分复制到sds,需要时对sds进行扩展 sds sdscpylen(sds s, const char *t, size_t len); //将一个给定C字符串复制到给定sds sds sdscpy(sds s, const char *t); //将已给 C字符串追加到给定sds末尾,并将sds内容格式化打印到另一给定C字符串 sds sdscatvprintf(sds s, const char *fmt, va_list ap); #ifdef __GNUC__ sds sdscatprintf(sds s, const char *fmt, ...) __attribute__((format(printf, 2, 3))); #else sds sdscatprintf(sds s, const char *fmt, ...); #endif sds sdscatfmt(sds s, char const *fmt, ...);//字符串格式化输出 sds sdstrim(sds s, const char *cset);//对给定sds缩剪 void sdsrange(sds s, int start, int end);//对给定sds进行按范围截取 void sdsupdatelen(sds s);//更新sds长度 void sdsclear(sds s);//清除sds内容,初始化为"" int sdscmp(const sds s1, const sds s2);//比较两个sds sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);//对sds分割子字符串 void sdsfreesplitres(sds *tokens, int count);//释放子串数组 void sdstolower(sds s);//将给定sds内容转换为小写字符 void sdstoupper(sds s);//将给定sds内容转换为大写字符 sds sdsfromlonglong(long long value);//创建数组sds?? sds sdscatrepr(sds s, const char *p, size_t len); sds *sdssplitargs(const char *line, int *argc);//参数拆分 sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);//字符映射 sds sdsjoin(char **argv, int argc, char *sep); //以分隔符连接字符串子数组构成新的字符串 /* Low level functions exposed to the user API */ /* 开放给使用者的API */ sds sdsMakeRoomFor(sds s, size_t addlen);//对给定sds对应的sdshdr结构的buf扩展 void sdsIncrLen(sds s, int incr);//增加sds的长度 sds sdsRemoveFreeSpace(sds s);//将buf多余的空间释放掉 size_t sdsAllocSize(sds s);//计算给定的sds的buf的内存空间大小 #endif </span>
sds.c:
<span style="font-size:18px;">//创建一个指定长度的sds,接受一个C字符串作为初始值。创建初始化时buf剩余空间buf为0,在下次追加时按照给定扩展算法,更新free sds sdsnewlen(const void *init, size_t initlen) { struct sdshdr *sh; if (init) { sh = zmalloc(sizeof(struct sdshdr)+initlen+1); } else { sh = zcalloc(sizeof(struct sdshdr)+initlen+1); } if (sh == NULL) return NULL; sh->len = (int)initlen; sh->free = 0; if (initlen && init) memcpy(sh->buf, init, initlen);//从源init所指的内存地址的起始位置开始拷贝initlen个字节到目标buf所指的内存地址的起始位置中 sh->buf[initlen] = '\0';//填充buf的末尾 return (char*)sh->buf;//返回C字符串 buf } /* Create an empty (zero length) sds string. Even in this case the string * always has an implicit null term. */ //创建一个空sds。(始终有一个\0字符) sds sdsempty(void) { return sdsnewlen("",0); } /* Create a new sds string starting from a null termined C string. */ sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); }</span>
扩容函数:(增加buf的free空间):如果原来free足够用则直接返回。否则,如果buf->len+addlen 小于SDS_MAX_PREALLOC(1M),则buf总空间扩为len+addlen的两倍;否则buf总空间扩为len+SDS_MAX_PREALLOC的两倍。 这也是sds由于char*的主要原因之一。在sds追加内容的时候,按需要触发扩容操作,并预留free的剩余空间,这样在下次追加时当free足够大时就可以不用再次重新分配空间(realloc),从而提高性能。以空间换时间,类似于动态数组vector。
对于预留的空间,可用 sdsRemoveFreeSpace(sds s)函数释放归还。然而free剩余空间的释放不是自动进行的,这可以通过设置系统,比如让系统定时自动释放掉free的剩余空间。
<span style="font-size:18px;">//扩容(增加buf的free空间):如果原来free足够用则直接返回。否则,如果buf->len+addlen 小于SDS_MAX_PREALLOC(1M),则buf总空间扩为len+addlen的两倍;否则buf总空间扩为len+SDS_MAX_PREALLOC的两倍 sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; size_t free = sdsavail(s);//现有的free size_t len, newlen; if (free >= addlen) return s; len = sdslen(s); sh = (void*) (s-(sizeof(struct sdshdr))); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); if (newsh == NULL) return NULL; newsh->free = (int)(newlen - len); return newsh->buf; } /* Reallocate the sds string so that it has no free space at the end. The * contained string remains not altered, but next concatenation operations * will require a reallocation. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ //去掉传入sds的free。 下次在该sds末尾追加时需要额外分配空间 sds sdsRemoveFreeSpace(sds s) { struct sdshdr *sh; sh = (void*) (s-(sizeof(struct sdshdr))); sh = (struct sdshdr *)zrealloc(sh, sizeof(struct sdshdr)+sh->len+1); sh->free = 0; return sh->buf; } </span>
<span style="font-size:18px;">//增加buf的len(buf的free对应的减少incr的长度),当incr为负数时,进行反向操作。用于比如在sds后追加了字符串(free足够大,没有触发重新的内存分配) //每次更新len之后都需要移动\0到对应位置 void sdsIncrLen(sds s, int incr) { struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); if (incr >= 0) assert(sh->free >= (unsigned int)incr);//从free分配到len,确保free大等于incr else assert(sh->len >= (unsigned int)(-incr));//从len分配到free,确保len大等于-incr sh->len += incr; sh->free -= incr; s[sh->len] = '\0'; } </span>
sdscpylen:将给定C串复制到指定sds(覆盖原有内容),需要时扩容,t为二进制安全的字符串,因此需要传入其长度,不能通过strlen函数计算(strlen函数计算长度时遇到\0则结束),如果给定的字符串为非二进制安全的,则不需要传入len参数。
<span style="font-size:18px;">sds sdscpylen(sds s, const char *t, size_t len) { struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); size_t totlen = sh->free+sh->len; if (totlen < len) { s = sdsMakeRoomFor(s,len-sh->len); if (s == NULL) return NULL; sh = (void*) (s-(sizeof(struct sdshdr))); totlen = sh->free+sh->len; } memcpy(s, t, len); s[len] = '\0'; sh->len = (int)len; sh->free = (int)(totlen-len); return s; }</span>
把数值转换为字符串存放到s指定位置 的辅助函数。 s指向的字符串长度至少等于SDS_LLSTR_SIZE,返回转换所得的以\0结尾的字符串的长度
<span style="font-size:18px;">int sdsll2str(char *s, long long value) { char *p, aux; unsigned long long v; size_t l; /* Generate the string representation, this method produces * an reversed string. */ //把数值通过取余逐位的存入字符串(为逆序) v = (value < 0) ? -value : value;//取value绝对值 p = s; do { *p++ = '0'+(v%10);//字符'0'加偏移量到对应数值的字符 v /= 10; } while(v); if (value < 0) *p++ = '-';//若是负数,最后加上 符号 /* Compute length and add null term. */ l = p-s; *p = '\0'; /* Reverse the string. */ p--; //字符串逆转(首尾字符交换) while(s < p) { aux = *s; *s = *p; *p = aux; s++; p--; } return (int)l; }</span>
sds sdscatfmt(sds s, char const *fmt, ...):字符串格式化输出,输入原字符串,格式,参数
字符串格式化输出到sds,输入原字符串,格式,各需要格式化到sds的参数 */
例: sds x="--";
x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN,LLONG_MAX);
memcmp(x,"--Hello Hi! World -9223372036854775808,9223372036854775807--",60) == 0)
<span style="font-size:18px;">sds sdscatfmt(sds s, char const *fmt, ...) { struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); size_t initlen = sdslen(s); const char *f = fmt; int i; va_list ap; va_start(ap,fmt); f = fmt; /* Next format specifier byte to process. */ i = initlen; /* Position of the next byte to write to dest str. */ //关键再次,以此比较输入的格式类型 while(*f) { char next, *str; unsigned int l; long long num; unsigned long long unum; /* Make sure there is always space for at least 1 char. */ if (sh->free == 0) { s = sdsMakeRoomFor(s,1); sh = (void*) (s-(sizeof(struct sdshdr))); } switch(*f) { case '%': /*如果是%,记住百分号后面的类型操作值*/ next = *(f+1); f++; switch(next) { case 's': case 'S': str = va_arg(ap,char*); //判断普通的str,还是sds类型,计算长度的方法不一样 l = (next == 's') ? strlen(str) : sdslen(str); if (sh->free < l) { s = sdsMakeRoomFor(s,l); sh = (void*) (s-(sizeof(struct sdshdr))); } //如果是字符串,直接复制到后面 memcpy(s+i,str,l); sh->len += l; sh->free -= l; i += l; break; case 'i': case 'I': if (next == 'i') num = va_arg(ap,int); else num = va_arg(ap,long long); { char buf[SDS_LLSTR_SIZE]; //如果是数字,调用添加数值字符串方法 l = sdsll2str(buf,num); if (sh->free < l) { s = sdsMakeRoomFor(s,l); sh = (void*) (s-(sizeof(struct sdshdr))); } memcpy(s+i,buf,l); sh->len += l; sh->free -= l; i += l; } break; case 'u': case 'U': //无符号整型同上 if (next == 'u') unum = va_arg(ap,unsigned int); else unum = va_arg(ap,unsigned long long); { char buf[SDS_LLSTR_SIZE]; l = sdsull2str(buf,unum); if (sh->free < l) { s = sdsMakeRoomFor(s,l); sh = (void*) (s-(sizeof(struct sdshdr))); } memcpy(s+i,buf,l); sh->len += l; sh->free -= l; i += l; } break; default: /* Handle %% and generally %<unknown>. */ s[i++] = next; sh->len += 1; sh->free -= 1; break; } break; default: //非操作类型,直接单字符添加 s[i++] = *f; sh->len += 1; sh->free -= 1; break; } f++; } va_end(ap); /* Add null-term */ s[i] = '\0'; return s; } </span>
<span style="font-size:18px;">//将给定的sds从左右两边剪去连续存在于cset中的所有字符 sds sdstrim(sds s, const char *cset) { struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); char *start, *end, *sp, *ep; size_t len; sp = start = s;//指向sds的头部 ep = end = s+sdslen(s)-1;//指向sds的最后一个字符 while(sp <= end && strchr(cset, *sp)) sp++;// 从sds的左边开始跳过所有连续在cset存在的字符 while(ep > start && strchr(cset, *ep)) ep--;//从sds的右边开始跳过所有连续在cset中存在的字符 len = (sp > ep) ? 0 : ((ep-sp)+1);//如此计算剩余的字符数:两个指针相减。如果左右移动已经交叉,则剩余空 if (sh->buf != sp) memmove(sh->buf, sp, len);//更新buf sh->buf[len] = '\0'; sh->free = sh->free+(int)(sh->len-len); sh->len = (int)len; return s; }</span>
小结:
对比C字符串,sds有以下特性:
* 可以高效的执行长度计算
* 可以高效的执行追加操作
* 二进制安全
sds会为追加操作进行优化: 加快追加操作的速度,并降低内存分配次数,代价是多占用了内存空间(buf的free预留空间),而且这些内存不会被主动释放。
相关文章推荐
- redis源码分析之简单动态字符串sds
- Redis源码阅读笔记(1)——简单动态字符串sds实现原理
- (一)redis源码学习之简单动态字符串(SDS)
- Redis源码解析:01简单动态字符串SDS
- 【redis源码分析】动态字符串--sds
- Redis源码剖析(十)简单动态字符串sds
- redis源码分析(二)、sds动态字符串学习总结
- 结合redis设计与实现的redis源码学习-2-SDS(简单动态字符串)
- Redis源码剖析--简单动态字符串sds
- Redis源码学习——简单动态字符串SDS(Simple Dynamic String)
- Redis源码分析二、Redis简单动态字符串
- Redis源代码分析之五:简单动态字符串——Sds
- Redis 源码解析 string内部实现原理之简单动态字符串SDS
- redis源码分析笔记2- redis的数据类型-动态字符串sds
- Redis学习——SDS字符串源码分析
- Redis -- 1、简单动态字符串(sds)
- redis源码解析1-简单动态字符串
- Redis源码阅读笔记(1)-- 动态字符串sds
- Redis源码剖析和注释(二)--- 简单动态字符串
- redis数据结构之一-简单动态字符串SDS