SDS简单动态字符串

81 阅读8分钟

什么是简单动态字符串? Redis没有直接使用C语言的字符串,而是自己封装了一种字符串,叫SDS(简单动态字符串)

哪些地方使用到了SDS? 在存储数据中,redis中键值对中键或值可以SDS,如果值是一个链表,链表中包含多个SDS,SDS类型于里面的最基本数据类型

除了存储数据,还有哪些地方使用到了SDS? SDS还被用作缓冲区:AOF时作为AOF的缓冲区,以及客户端输入缓冲区

为什么要使用SDS,而不使用C字符串? SDS的C源码结构是什么样的?SDS是如何实现的? 3.2之前的版本:

sturct sdshdr{
	unsigned int len;//已使用的字符串长度
	unsigned int free;//未使用的字符串长度
	char buf[];//字符串数组的引用
}

3.2之后的版本:根据字符串的长度,有5种结构体

C字符串与SDS有哪些区别?

C语言中分配N+1个字符串的空间,能存储N个字符串,空字符结尾 C语言获取一个字符串的长度,需要遍历一个字符串,时间复杂度O(n) 在SDS中直接访问len属性,就可以得到SDS的长度,时间复杂度为O(1) SDS在获取字符串长度时,不会有多的性能损耗,比如命令strlen可以获取键的长度,时间复杂度为O(1)

Redis为什么要设计5种不同的数据结构?

SDS以什么作为结尾标识? 空字符串

有效长度中包括空字符串吗? 不包括

为什么要使用空字符串作为结尾标识? SDS可以直接重用C字符串函数里面的函数 比如:如果有一个执行SDS的指针s,我们使用C中库函数<stdio.h>/printf函数执行printf函数:printf("%s",s->buf);打印字符串值"Redis",

Redis中为什么要使用SDS? 可以直接通过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;

}

Redis为什么能够避免修改字符串带来的内存分配? 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可以重用一部分<String.h>库定义的函数 针对的是什么? 文本数据的优化,Redis就不用自己再实现文本数据的字符串函数操作

总结: C字符串与SDS有哪些区别? 在获取长度的时间复杂度上? C字符串O(n),SDS O(1) API是否安全? C字符串的操作需要手动分配内存,释放内存,否则可能缓存溢出或者缓存内存泄露 SDS安全,自动的机制扩容机制,当容量不够时,会自动扩容 内存分配的优化次数? C字符串添加或缩短一次,就会进行一次内存分配,是比较耗时的操作 SDS,在添加和修改的时候都做了优化,在添加如果容量不足导致扩容,可触发空间预分配机制,为字符串分配多的未使用空间,下次继续添加字符,就可能不需要再分配内存 在字符串缩短的时候,会触发惰性空间释放机制,不会重新分配内存释放缩短的字符空间,下次如果要继续添加字符,可能也不需要再分配内存 是否二进制安全? C字符串二进制不安全,原因是二进制中有空字符,会导致提前截断 SDS中,字符串的结束是以len作为结束长度,字符串中有空字符也不会被截断 能使用<String.h>中的库函数吗? SDS可以使用一部分,原因是SDS还是会在结尾加一个空字符,对于文本字符串,能够直接重用<String.h>库中的部分函数

回顾重点: 比起C字符串,SDS具备的优点: 1.直接获取字符串长度 2.杜绝缓冲区溢出,原因是有自动的扩容机制,不需要手动扩容 3.减少了修改字符串带来的分配内存次数,分配内存次数会带来过的的损耗 4.二进制安全,以len大小作为字符串结尾,而不是空字符串 5.兼容C部分字符串函数,字符串结尾为空字符,处理文本数据,还是可以使用部分C库函数