Redis 3.0.4 简单动态字符串(sds)
字符串是Redis最常见的结构,Redis没有用C中的字符串,是自己构建的simple dynamic string来表示字符串
字符串的源码在sds.c/sds.h
- sds的基本结构
struct sdshdr { unsigned int len; //len表示当前buf中已使用字节长度 unsigned int free;//表示buf中未使用字节的长度 char buf[];//字节数组,保存字符串 };
- SDS与字符串的区别
1.常数复杂度获取字符串长度
因为sds的结构中len表示当前buf已经使用的字节长度,所以获取长度是只需要返回sds->len,时间复杂度是o(1),而C语言中必须便利整个字符串,直到遇到结尾空字符为止,所以时间复杂度是o(n).
2.杜绝缓冲区溢出
因为C字符串不记录自身长度,所以字符串操作容易出现缓冲区溢出现象,sds的空间分配策略避免了缓冲区溢出的可能性。
3.减少修改字符串时带来的内存重新分配
因为C字符串的长度和底层数组的长度存在关联性 ,内存重分配需要执行系统调用,执行系统调用会从用户态陷入内核态,底层依旧是通过中断实现的,所以需要保持上下文,栈信息,这个过程有时不能被中断的,所以是一个很耗时的过程。
redis使用了空间预分配和惰性空间释放两种策略,
空间预留,就是在字符串增长操作中,当需要扩大空间时,会对sds分配额外的空间。额外分配的空间又修改之后sds的长度决定
1.当修改之后的长度小于SDS_MAX_PREALLOC(1M),那么分配的空间为len*2;
2.当修改之后的长度大于SDS_MAX_PREALLOC(1M),那么分配的空间为len+1M;
关于分配空间源码调用的事zrealloc底层调用的事realloc,如果原先的内存尾部有足够大的空间,则直接在原来的内存块尾部新增内存,如果原来的内存块尾部内存不足,或者原先的内存块无法改变大小,realloc将重新分配另一块size大小的内存,并把原来那块内存内容复制到新的内存块上。可参考:C—动态内存分配之malloc与realloc的区别
/* Enlarge the free space at the end of the sds string so that the caller * is sure that after calling this function can overwrite up to addlen * bytes after the end of the string, plus one more byte for nul term. * * Note: this does not change the *length* of the sds string as returned * by sdslen(), but only the free buffer space we have. */ //判断sds剩余空间是否满足addlen长度 sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; size_t free = sdsavail(s); //返回s未使用空间长度 size_t len, newlen; if (free >= addlen) return s; //需要扩大空间,如果修改之后的长度小于SDS_MAX_PREALLOC,则分配2*len //如果大于SDS_MAX_PREALLOC,则再加上SDS_MAX_PREALLOC 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 = newlen - len; return newsh->buf; }
2.惰性空间释放
用于优化sds字符串缩短操作,当缩短字符串时,并不会使用内存重新分配来回收缩短后多出来的字节,而是使用free属性将这些字节记录下来,待将来使用。
//出去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; ep = end = s+sdslen(s)-1; while(sp <= end && strchr(cset, *sp)) sp++; while(ep > start && strchr(cset, *ep)) ep--; len = (sp > ep) ? 0 : ((ep-sp)+1); if (sh->buf != sp) memmove(sh->buf, sp, len); sh->buf[len] = ‘\0‘; sh->free = sh->free+(sh->len-len); sh->len = len; return s; }
这块是字符串缩短的代码,找了一些资料都是说是去除sds字符串中cset中包含的所有字符,但是我看源码理解的是去除sds字符中首尾中包含cset中包含的字符,比如
* Example: * * s = sdsnew("AA...AA.a.aa.Hello.World:::"); * s = sdstrim(s,"Aa. :"); * printf("%s\n", s); * OutPut:Hello.World * 字符串中的.是去不掉的,然后求解~
4.二进制安全
sds用二进制的方式来处理存放在buf数组中的数据,程序不会对数据做任何限制、过滤、或者假设,数据写入是什么样的,读取时就是什么样。缓解了C语言只能存储文本文件的尴尬,sds不仅可以保存文本数据, 还可以保存任意格式的二进制数据,并且用len长度作为字符串结尾的约束。
5.兼容部分C字符串函数
虽然sds都是二进制安全的,但是还是遵循C字符串一空字符结尾,这是为了那些保存文本文件的sds可以重用一部分<string.h>库定义的函数。
C 字符串和 SDS 之间的区别
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度为 O(N) 。 | 获取字符串长度的复杂度为 O(1) 。 |
API 是不安全的,可能会造成缓冲区溢出。 | API 是安全的,不会造成缓冲区溢出。 |
修改字符串长度 N 次必然需要执行 N 次内存重分配。 | 修改字符串长度 N 次最多需要执行 N 次内存重分配。 |
只能保存文本数据。 | 可以保存文本或者二进制数据。 |
可以使用所有 <string.h> 库中的函数。 | 可以使用一部分 <string.h> 库中的函数。 |