Redis中简单动态字符串SDS详解

什么是简单动态字符串?


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[];//字符串数组的引用
    }

Redis中简单动态字符串SDS详解_第1张图片

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

Redis中简单动态字符串SDS详解_第2张图片
     

Redis中简单动态字符串SDS详解_第3张图片


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

 Redis中简单动态字符串SDS详解_第4张图片

 
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",


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可以重用一部分库定义的函数
针对的是什么?
    文本数据的优化,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库函数

你可能感兴趣的:(redis,数据库,缓存)