哪些地方使用到了SDS?
在存储数据中,redis中键值对中键或值可以SDS,如果值是一个链表,链表中包含多个SDS,SDS类型于里面的最基本数据类型
除了存储数据,还有哪些地方使用到了SDS?
SDS还被用作缓冲区:AOF时作为AOF的缓冲区,以及客户端输入缓冲区
SDS的C源码结构是什么样的?SDS是如何实现的?
3.2之前的版本:
sturct sdshdr{
unsigned int len;//已使用的字符串长度
unsigned int free;//未使用的字符串长度
char buf[];//字符串数组的引用
}
3.2之后的版本:根据字符串的长度,有5种结构体
C语言中分配N+1个字符串的空间,能存储N个字符串,空字符结尾
C语言获取一个字符串的长度,需要遍历一个字符串,时间复杂度O(n)
在SDS中直接访问len属性,就可以得到SDS的长度,时间复杂度为O(1)
SDS在获取字符串长度时,不会有多的性能损耗,比如命令strlen可以获取键的长度,时间复杂度为O(1)
有效长度中包括空字符串吗?
不包括
SDS可以直接重用C字符串函数里面的函数
比如:如果有一个执行SDS的指针s,我们使用C中库函数/printf函数执行printf函数:printf("%s",s->buf);打印字符串值"Redis",
可以直接通过len属性拿到长度,不需要遍历
不会出现缓存区溢出
避免修改字符串带来过多的内存分配SDS会出现字符串缓冲区溢出吗?为什么?
C语言中为什么会出现缓冲区溢出?如果程序员要拼接两个字符串,可以使用strcat(s1,s3)函数,但忘记为s1分配做够空间,导致s3的字符串会覆盖修改s2中的字符串,C字符串需要程序员手动分配好空间
SDS会自动分配空间,当要对SDS修改的时候,会先检查SDS的空间是否满足需要修改的要求,不满足会将空间自动扩容到修改需要的大小,然后再修改
在C源码中,SDS的检查和扩容封装在sdsMakeRoomFor函数中:
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 获取剩余长度
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 剩余长度>要增加的长度直接返回
if (avail >= addlen) return s;
// 获取有效长度
len = sdslen(s);
// sds指针
sh = (char*)s-sdsHdrSize(oldtype);
// 获取新字符串的总长度
newlen = (len+addlen);
// 如果新长度的总小于最大预分配长度,则进行两倍扩容
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;//否则追加分配长度
type = sdsReqType(newlen);
// SDS类型5转换为类型8
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
//类型没转换
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
//类型转换
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
C中字符串不记录自身的长度,N长度的C字符串,底层需要N+1的字符串数组,所以每次添加或删除一个字符,内存中都需要重新为C字符串分配一次内存
在C语言中,如果要进行字符串拼接,拼接之前需要手动分配做够长度的底层数组,否则会缓冲区溢出,如果进行字符串的截断,在截断之前没有手动的分配内存,释放不使用的那部分空间,会产生内存泄露
内存的分配涉及复杂的算法,可能需要系统调用,内存分配是比较耗时的操作
总结:
C中每一次扩大字符串或缩小字符串,都需要重新分配内存,是比较耗时的操作
在SDS中,buf数组的长度不是字符串长度+1,buf中可以包含未使用的空间,用属性free记录,因为SDS有未使用的空间,实现了空间预分配和惰性空间释放两种优化:
什么时候会触发?
SDS的API修改SDS的时候,并且需要对SDS进行扩容
那什么时候SDS会进行扩容?
SDS的free空间不够时
那在扩展的时候会做些什么?
除了分配修改所需要的空间,还会为SDS分配额外的未使用的空间
会分配多大的额外未使用的空间呢?
要分为两种情况,有效长度小于1MB和大于1MB
小于1MB呢?
新字符串的有效长度小于1MB,额外空间和新字符串的有效长度一样,比如新的有效长度为13,那么SDS的实际长度为len+free+1=13+13+1=27字节
大于1MB呢?每次分配1MB的额外空间,比如新的有效长度为20MB,那么SDS实际分配长度为20MB+1MB+1byte
比如:s:执行sds(s," Cluster")后:
再次sdscat(s," Tuorial"),不需要执行重新分配内存,free的13字节已经做够
总结:通过空间预分配策略,可以减少字符串增长的内存空间分配次数
什么是惰性空间释放?
什么时候会触发惰性空间释放?
当SDS字符串进行删除缩短操作时
原理是什么?
删除或者缩短字符串后,不会立即重新分配内存释放删除字符串的那部分空间
比如:执行api sdstrim(s,"XY") 删除字符串中的X和Y
删除前和删除后buf的实际空间大小一致,因为删除操作并没有重新分配内存释放多出来的8个空间
总结:
通过惰性空间释放机制,避免了SDS缩短字符串时去重新分配内存释放内存的操作,多出来的空间可作为字符串增长的空间
最后,如何避免惰性空间释放机制导致内存浪费?
提供了相应的API,真正的释放内存
C字符串二进制安全吗?
C字符串中不能有空字符,空字符只能作为字符串的结尾,所以C字符串只能保存文本,不能保存图片,视频,压缩文件等二进制文件,二进制文件中有空字符串
SDS可以保存二进制文件吗(图片,视频,压缩文件)?
可以
为什么能够保存二进制文件?
SDS使用的是len属性来判断字符串结尾,故允许数据中存在空字符,故能存储二进制数据SDS兼容哪些C字符串函数?
为什么可以兼容?
SDS会在字符串的结尾多设置一个空字符串
为什么要兼容?
让保存文本数据的SDS可以重用一部分
针对的是什么?
文本数据的优化,Redis就不用自己再实现文本数据的字符串函数操作
总结:
C字符串与SDS有哪些区别?
在获取长度的时间复杂度上?
C字符串O(n),SDS O(1)
API是否安全?
C字符串的操作需要手动分配内存,释放内存,否则可能缓存溢出或者缓存内存泄露
SDS安全,自动的机制扩容机制,当容量不够时,会自动扩容
内存分配的优化次数?
C字符串添加或缩短一次,就会进行一次内存分配,是比较耗时的操作
SDS,在添加和修改的时候都做了优化,在添加如果容量不足导致扩容,可触发空间预分配机制,为字符串分配多的未使用空间,下次继续添加字符,就可能不需要再分配内存
在字符串缩短的时候,会触发惰性空间释放机制,不会重新分配内存释放缩短的字符空间,下次如果要继续添加字符,可能也不需要再分配内存
是否二进制安全?
C字符串二进制不安全,原因是二进制中有空字符,会导致提前截断
SDS中,字符串的结束是以len作为结束长度,字符串中有空字符也不会被截断
能使用中的库函数吗?
SDS可以使用一部分,原因是SDS还是会在结尾加一个空字符,对于文本字符串,能够直接重用库中的部分函数
回顾重点:
比起C字符串,SDS具备的优点:
1.直接获取字符串长度
2.不可能发生缓冲区溢出,原因是有自动的扩容机制,不需要手动扩容
3.减少了修改字符串带来的分配内存次数,分配内存次数会带来过的的损耗
4.二进制安全,以len大小作为字符串结尾,而不是空字符串
5.兼容C部分字符串函数,字符串结尾为空字符,处理文本数据,还是可以使用部分C库函数