zoukankan      html  css  js  c++  java
  • Redis 设计与实现(第二章) -- SDS

    概述


    1.SDS介绍

    2.SDS API

    3.SDS与C的比较

    SDS介绍

    在C语言中,用来表达字符串的方式通常有两种,

    char *buf1="redis";

    char buf2[]="redis";

    方式1,通过一个char指针指向一个字符串字面量,起内容无法改变,即无法通过buf1[1]='c'来改变内容,如果需要改变,需要将指针重新赋值,指向其他内存空间;

    方式2,char数组,末尾有一个‘’来代表结束,但是不携带长度信息,在字符串操作时,比如strcat,可能会导致缓存区溢出。

    在Redis里面C中的字符串字面量一般只用于不需要对字符串值修改的地方,比如打印日志:

    redisLog(REDIS_WARNING,'redis now is ready to exit,bye bye...')

    当需要对字符串值进行修改时,会使用SDS结构来表示字符串值;

    在Redis中,SDS用于很多地方,比如数据库中的键值,缓冲区,AOF缓冲区等。 可以说SDS是redis的基础。

    可以看一下SDS的数据结构,在sds.h文件:

    struct sdshdr {
        unsigned int len;  //记录buf数组中已经使用的字节数量,等于SDS的长度
        unsigned int free;  //buf数组中未使用的字节数量
        char buf[];   //buf数组,用于存储字符串
    };

    可以看到,sds的数据结构多了len和free字段,后面会讲到这两个字段的主要用途。下图说明存储结构:

    SDS API

    创建

    sds sdsnewlen(const void *init, size_t initlen) {
        struct sdshdr *sh;
    
        if (init) {
            sh = zmalloc(sizeof(struct sdshdr)+initlen+1); //初始化,+1用于存储""
        } else {
            sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
        }
        if (sh == NULL) return NULL;
        sh->len = initlen;  //设置长度
        sh->free = 0;  //free为0
        if (initlen && init)
            memcpy(sh->buf, init, initlen);  //将init中的内存拷贝到sds中
        sh->buf[initlen] = '';  //结尾符
        return (char*)sh->buf;
    }

    复制

    sds sdsdup(const sds s) {
        return sdsnewlen(s, sdslen(s));
    }

    释放

    void sdsfree(sds s) {
        if (s == NULL) return;
        zfree(s-sizeof(struct sdshdr));
    }

    清除

    void sdsclear(sds s) {
        struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
        sh->free += sh->len; //free回收
        sh->len = 0;  //len变为0
        sh->buf[0] = '';
    }

    获取长度

    static inline size_t sdslen(const sds s) {
        struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
        return sh->len;
    }
    
    static inline size_t sdsavail(const sds s) {
        struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
        return sh->free;
    }

    sdscat

    sds sdscatlen(sds s, const void *t, size_t len) {
        struct sdshdr *sh;
        size_t curlen = sdslen(s);
    
        s = sdsMakeRoomFor(s,len);  //调用扩容
        if (s == NULL) return NULL;
        sh = (void*) (s-(sizeof(struct sdshdr)));
        memcpy(s+curlen, t, len);
        sh->len = curlen+len;
        sh->free = sh->free-len;
        s[curlen+len] = '';
        return s;
    }
    
    /* Append the specified null termianted C string to the sds string 's'.
     *
     * 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 sdscat(sds s, const char *t) {
        return sdscatlen(s, t, strlen(t));
    }

    扩容

    sds sdsMakeRoomFor(sds s, size_t addlen) {
        struct sdshdr *sh, *newsh;
        size_t free = sdsavail(s);
        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)  //小于1M则扩展为新的长度的两倍
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;  //大于1M则扩容为字符串长度加上1M
        newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
        if (newsh == NULL) return NULL;
    
        newsh->free = newlen - len;
        return newsh->buf;
    }

    #define SDS_MAX_PREALLOC (1024*1024)   //定义MAX_PREALLOC

     

    SDS与C的比较

    1.获取字符串长度复杂度为常数

    在C中,获取char数组的长度时,需要遍历整个数组,直到最后一个。然后使用一个计数器累加。这样的复杂度为O(N)

    对于SDS,由于使用len属性保存了字符串长度,所以获取长度的复杂度为O(1).

    2.杜绝缓冲区溢出

    针对strcat函数,假设需要使用strcat(char *dest,const char * src),将src中的字符串拼接到dest后

    在C中,由于没有保存字符串长度,所以strcat假设在用户执行字符串拼接函数时,已经为dest分配了足够的内存,保证能够容纳src中的所有内容,所以一旦假设不成立,就会造成缓冲区溢出。举个例子,假设程序中两个在内存中紧挨着的字符串s1和s2,s1保存了字符串“redis”,s2保存了字符串“mongo”,这时程序员执行strcat(s1,“cluster”),并且在执行前忘了扩展s1的内存空间,这时s2就会被替换为cluster。

    在SDS中,拼接字符串的函数为sdscat,由于保存了字符串的长度和剩余的空间,在执行前会先判断当前的空间是否容得下要拼接的字符串长度,如果不符合会执行空间的扩容,扩容后再进行字符串拼接。假设当前s1保存了redis,且剩余空间为0,如下:

     这时执行sdscat(s1,"cluster"),sds会先判断剩余的空间是否能够容纳cluster的长度,发现不够,这时SDS会进行扩容,扩容后再进行拼接处理。

    如上图所示,SDS不仅对字符串进行了拼接,而且分配了13个字节长的空间,恰好拼接后的长度也是13,这是为什么呢?   和SDS的内存分配策略有关,后面会讲到。

    3.减少修改字符串时带来的内存重分配次数

    在C中,因为不记录自身字符串的长度,所以对于一个包含N个字符的数组来说,底层实现为N+1长的数组,额外的空间用于保存空字符。基于这种关系,所以在字符串增长或缩短的时候,程序都需要为数组进行一次内存重分配操作,这样对于Redis这种高并发操作是不能忍受的。

    在SDS中,会有free属性来记录剩余的空间字节数,字符串长度和数组空间没有任何关联。所以在SDS中,buf数组长度不一定就是字符数+1,里面可能有未使用的字节空间。

    通过未使用空间,SDS实现了空间预分配和惰性释放的优化策略。

    空间预分配:

    用于优化SDS的字符串增长操作,当对一个SDS进行增长修改时,并且需要对SDS空间进行扩展时,程序不仅会分配修改所必须的空间,还会为SDS分配额外未使用的空间,具体策略如下:

    •      如果修改后,sds的长度小于1M,那么程序将会分配同样的未使用空间给SDS。这时free和len属性相同。
    •      如果修改后,SDS的长度大于等于1M,则会分配1M的空间。

    在扩展SDS前,如果内存足够使用,则不需要对内存进行重分配,这样将内存重分配的次数充N次减少到了最多N次。

    惰性释放:

    惰性释放用于字符串缩短操作,当一个字符串缩短时,并不会立即回收多出来的字节,而是使用使用free属性将这些数保存起来,供后面使用。这样的好处是下次如果再进行增长操作时,不需要再次内存重分配。这样的弊端在哪呢?  很明显,可能会造成内存的浪费。 SDS也提供了相应的API,可以在有需要时,真正释放未使用的空间。

    4.保存的二进制文件,安全

    C里面保存的是字符串数据,SDS的buf数组保存的是二进制;

    比如针对字符串“a b c”,对于C用的函数只能失败a出来,默认任务空为结束符了。而SDS存储格式为二进制,所以无论什么样的格式都不会有影响。 

    5.能够兼容一部分C的字符串函数

    可以使用<string.h>中的一部分函数 

  • 相关阅读:
    js使用sessionStorage、cookie保存token
    在vue项目中的main.js中直接使用element-ui中的Message 消息提示、MessageBox 弹框、Notification 通知
    git报错:'fatal:remote origin already exists'怎么处理、附上git常用操作以及说明
    Module build failed: TypeError: loaderContext.getResolve is not a function
    Git相关命令
    用户反馈录
    静雅斋数学[目录]
    探索|尝试编制高质量的跟踪训练试卷
    直线参数方程何时必须化为标准形式
    快速高效的做三角函数图像
  • 原文地址:https://www.cnblogs.com/dpains/p/7598087.html
Copyright © 2011-2022 走看看