Redis源码分析(五)——简单动态字符串(sds)

Sds(Simple Dynamic String)是Redis底层所使用的字符串表示,它被用在几乎所有的Redis模块中。


Redis是一个键值对数据库(K-V DB),数据库的值可以是字符串、集合、列表等多种对象,而键则只能是字符串对象。    由于char*类型的功能单一,抽象层次低,不能高效的支持一些Redis常用的操作(比如对字符串的追加操作和长度计算操作,而在Redis中对字符串的追加以及计算长度都是很常见的操纵,每次对char*字符串进行追加都必然导致内存的重新分配(realloc)这将严重影响性能),另外Reids的字符串表示还应该是二进制安全的。   所以在Redis的内部绝大部分使用的都是sds而不是char*来表示字符串。


二进制安全字符串:二进制安全是一种主要用于字符串操作函数相关的计算机编程术语。其本质就是将字符串中的所有字符都看作 原始的、无任何特殊格式意义的数据流。将所有字符视为按照一串0和1的形式编码的二进制数据,而不会对'\0' ,'\n' 等“特殊”的字符赋予不同的特殊含义。

Sds字符串总是以\0作为结束(sds对应的sdshdr的len不包括末尾的\0),由于sdshdr保存了字符串的长度len,因此在sds的中间可以包括\0字符(二进制安全的)。
在对字符串sds的操作时,总是把目标的C字符串cher*(sds)转换为 sdshdr(相当于string对象)进行操(及时更新sdshdr的已有字符数len,剩余空间free和实际存放字符串的buf等成员参数),然后再返回更新后的sds。 这样就使得C的字符串cher*具有了类似高级string的各种操作方法,从而能够方便的对C字符串sds计算长度以及追加等复杂操作。

Rdis通过对sds封装为sdshdr结构体,为sds字符串的操作提供了大量的操作方法,使得在Rdis中方便高效的使用字符串。



以下对sds的主要API函数进行分析,具体见注释。

sds.c /sds.h :

<span style="font-size:18px;">typedef char *sds;     //sds是C char*的别名

//动态字符串结构体
struct sdshdr {
    unsigned int len;//当前buf已占用空间
    unsigned int free;//buf剩余空间
    char buf[];//实际保存字符串数据的地方
};

//计算sds的长度,返回的size_t类型的数值
static __inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

//计算sds剩余可用长度
static __inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}

//创建一个指定长度的sds,接受一个C字符串作为初始值
sds sdsnewlen(const void *init, size_t initlen);
//创建一个sds,初始化为给定C字符串
sds sdsnew(const char *init);
//创建一个只包含空白串""的sds
sds sdsempty(void);
//获取sds的长度
size_t sdslen(const sds s);
//复制给定sds
sds sdsdup(const sds s);
//释放给定sds
void sdsfree(sds s);
//获取给定sds剩余可用空间
size_t sdsavail(const sds s);
//将给定sds的buf扩张至指定长度,无内容的部分用\0填充
sds sdsgrowzero(sds s, size_t len);
//将给定sds扩展至给定长度,并将一个给定C字符串追加到sds末尾
sds sdscatlen(sds s, const void *t, size_t len);
//将给定C字符串追加到给定sds末尾
sds sdscat(sds s, const char *t);
//将一个给定sds追加到另一个sds末尾
sds sdscatsds(sds s, const sds t);
//将一个给定C字符串的len部分复制到sds,需要时对sds进行扩展
sds sdscpylen(sds s, const char *t, size_t len);
//将一个给定C字符串复制到给定sds
sds sdscpy(sds s, const char *t);
//将已给 C字符串追加到给定sds末尾,并将sds内容格式化打印到另一给定C字符串
sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);//字符串格式化输出
sds sdstrim(sds s, const char *cset);//对给定sds缩剪
void sdsrange(sds s, int start, int end);//对给定sds进行按范围截取
void sdsupdatelen(sds s);//更新sds长度
void sdsclear(sds s);//清除sds内容,初始化为""
int sdscmp(const sds s1, const sds s2);//比较两个sds
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);//对sds分割子字符串
void sdsfreesplitres(sds *tokens, int count);//释放子串数组
void sdstolower(sds s);//将给定sds内容转换为小写字符
void sdstoupper(sds s);//将给定sds内容转换为大写字符
sds sdsfromlonglong(long long value);//创建数组sds??
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc);//参数拆分
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);//字符映射 
sds sdsjoin(char **argv, int argc, char *sep); //以分隔符连接字符串子数组构成新的字符串  

/* Low level functions exposed to the user API */
/* 开放给使用者的API */ 
sds sdsMakeRoomFor(sds s, size_t addlen);//对给定sds对应的sdshdr结构的buf扩展
void sdsIncrLen(sds s, int incr);//增加sds的长度
sds sdsRemoveFreeSpace(sds s);//将buf多余的空间释放掉
size_t sdsAllocSize(sds s);//计算给定的sds的buf的内存空间大小

#endif
</span>

sds.c:

<span style="font-size:18px;">//创建一个指定长度的sds,接受一个C字符串作为初始值。创建初始化时buf剩余空间buf为0,在下次追加时按照给定扩展算法,更新free
sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
    if (sh == NULL) return NULL;
    sh->len = (int)initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);//从源init所指的内存地址的起始位置开始拷贝initlen个字节到目标buf所指的内存地址的起始位置中

    sh->buf[initlen] = '\0';//填充buf的末尾
    return (char*)sh->buf;//返回C字符串 buf
}

/* Create an empty (zero length) sds string. Even in this case the string
 * always has an implicit null term. */
//创建一个空sds。(始终有一个\0字符)
sds sdsempty(void) {
    return sdsnewlen("",0);
}

/* Create a new sds string starting from a null termined C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}</span>



扩容函数:(增加buf的free空间):如果原来free足够用则直接返回。否则,如果buf->len+addlen 小于SDS_MAX_PREALLOC(1M),则buf总空间扩为len+addlen的两倍;否则buf总空间扩为len+SDS_MAX_PREALLOC的两倍。 这也是sds由于char*的主要原因之一。在sds追加内容的时候,按需要触发扩容操作,并预留free的剩余空间,这样在下次追加时当free足够大时就可以不用再次重新分配空间(realloc),从而提高性能。以空间换时间,类似于动态数组vector。 对于预留的空间,可用 sdsRemoveFreeSpace(sds s)函数释放归还。然而free剩余空间的释放不是自动进行的,这可以通过设置系统,比如让系统定时自动释放掉free的剩余空间。


<span style="font-size:18px;">//扩容(增加buf的free空间):如果原来free足够用则直接返回。否则,如果buf->len+addlen 小于SDS_MAX_PREALLOC(1M),则buf总空间扩为len+addlen的两倍;否则buf总空间扩为len+SDS_MAX_PREALLOC的两倍
sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s);//现有的free
    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)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = (int)(newlen - len);
    return newsh->buf;
}

/* Reallocate the sds string so that it has no free space at the end. The
 * contained string remains not altered, but next concatenation operations
 * will require a reallocation.
 *
 * 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的free。 下次在该sds末尾追加时需要额外分配空间
sds sdsRemoveFreeSpace(sds s) {
    struct sdshdr *sh;

    sh = (void*) (s-(sizeof(struct sdshdr)));
    sh = (struct sdshdr *)zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
    sh->free = 0;
    return sh->buf;
}
</span>


<span style="font-size:18px;">//增加buf的len(buf的free对应的减少incr的长度),当incr为负数时,进行反向操作。用于比如在sds后追加了字符串(free足够大,没有触发重新的内存分配)
//每次更新len之后都需要移动\0到对应位置
void sdsIncrLen(sds s, int incr) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

    if (incr >= 0)
        assert(sh->free >= (unsigned int)incr);//从free分配到len,确保free大等于incr
    else
        assert(sh->len >= (unsigned int)(-incr));//从len分配到free,确保len大等于-incr
    sh->len += incr;
    sh->free -= incr;
    s[sh->len] = '\0';
}
</span>

sdscpylen:将给定C串复制到指定sds(覆盖原有内容),需要时扩容,t为二进制安全的字符串,因此需要传入其长度,不能通过strlen函数计算(strlen函数计算长度时遇到\0则结束),如果给定的字符串为非二进制安全的,则不需要传入len参数。
<span style="font-size:18px;">sds sdscpylen(sds s, const char *t, size_t len) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    size_t totlen = sh->free+sh->len;


    if (totlen < len) {
        s = sdsMakeRoomFor(s,len-sh->len);
        if (s == NULL) return NULL;
        sh = (void*) (s-(sizeof(struct sdshdr)));
        totlen = sh->free+sh->len;
    }
    memcpy(s, t, len);
    s[len] = '\0';
    sh->len = (int)len;
    sh->free = (int)(totlen-len);
    return s;
}</span>


把数值转换为字符串存放到s指定位置 的辅助函数。 s指向的字符串长度至少等于SDS_LLSTR_SIZE,返回转换所得的以\0结尾的字符串的长度

<span style="font-size:18px;">int sdsll2str(char *s, long long value) {
    char *p, aux;
    unsigned long long v;
    size_t l;

    /* Generate the string representation, this method produces
     * an reversed string. */
	//把数值通过取余逐位的存入字符串(为逆序)
    v = (value < 0) ? -value : value;//取value绝对值
    p = s;
    do {
        *p++ = '0'+(v%10);//字符'0'加偏移量到对应数值的字符
        v /= 10;
    } while(v);
    if (value < 0) *p++ = '-';//若是负数,最后加上 符号

    /* Compute length and add null term. */
    l = p-s;
    *p = '\0';

    /* Reverse the string. */
    p--;
	//字符串逆转(首尾字符交换)
    while(s < p) {
        aux = *s;
        *s = *p;
        *p = aux;
        s++;
        p--;
    }
    return (int)l;
}</span>

sds sdscatfmt(sds s, char const *fmt, ...)字符串格式化输出,输入原字符串,格式,参数
 字符串格式化输出到sds,输入原字符串,格式,各需要格式化到sds的参数 */  
   例:   sds x="--";  
          x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN,LLONG_MAX);
  memcmp(x,"--Hello Hi! World -9223372036854775808,9223372036854775807--",60) == 0)

<span style="font-size:18px;">sds sdscatfmt(sds s, char const *fmt, ...) {  
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));  
    size_t initlen = sdslen(s);  
    const char *f = fmt;  
    int i;  
    va_list ap;  
  
    va_start(ap,fmt);  
    f = fmt;    /* Next format specifier byte to process. */  
    i = initlen; /* Position of the next byte to write to dest str. */  
    //关键再次,以此比较输入的格式类型  
    while(*f) {  
        char next, *str;  
        unsigned int l;  
        long long num;  
        unsigned long long unum;  
  
        /* Make sure there is always space for at least 1 char. */  
        if (sh->free == 0) {  
            s = sdsMakeRoomFor(s,1);  
            sh = (void*) (s-(sizeof(struct sdshdr)));  
        }  
  
        switch(*f) {  
        case '%':  
            /*如果是%,记住百分号后面的类型操作值*/  
            next = *(f+1);  
            f++;  
            switch(next) {  
            case 's':  
            case 'S':  
                str = va_arg(ap,char*);  
                //判断普通的str,还是sds类型,计算长度的方法不一样  
                l = (next == 's') ? strlen(str) : sdslen(str);  
                if (sh->free < l) {  
                    s = sdsMakeRoomFor(s,l);  
                    sh = (void*) (s-(sizeof(struct sdshdr)));  
                }  
                //如果是字符串,直接复制到后面  
                memcpy(s+i,str,l);  
                sh->len += l;  
                sh->free -= l;  
                i += l;  
                break;  
            case 'i':  
            case 'I':  
                if (next == 'i')  
                    num = va_arg(ap,int);  
                else  
                    num = va_arg(ap,long long);  
                {  
                    char buf[SDS_LLSTR_SIZE];  
                    //如果是数字,调用添加数值字符串方法  
                    l = sdsll2str(buf,num);  
                    if (sh->free < l) {  
                        s = sdsMakeRoomFor(s,l);  
                        sh = (void*) (s-(sizeof(struct sdshdr)));  
                    }  
                    memcpy(s+i,buf,l);  
                    sh->len += l;  
                    sh->free -= l;  
                    i += l;  
                }  
                break;  
            case 'u':  
            case 'U':  
            //无符号整型同上  
                if (next == 'u')  
                    unum = va_arg(ap,unsigned int);  
                else  
                    unum = va_arg(ap,unsigned long long);  
                {  
                    char buf[SDS_LLSTR_SIZE];  
                    l = sdsull2str(buf,unum);  
                    if (sh->free < l) {  
                        s = sdsMakeRoomFor(s,l);  
                        sh = (void*) (s-(sizeof(struct sdshdr)));  
                    }  
                    memcpy(s+i,buf,l);  
                    sh->len += l;  
                    sh->free -= l;  
                    i += l;  
                }  
                break;  
            default: /* Handle %% and generally %<unknown>. */  
                s[i++] = next;  
                sh->len += 1;  
                sh->free -= 1;  
                break;  
            }  
            break;  
        default:  
            //非操作类型,直接单字符添加  
            s[i++] = *f;  
            sh->len += 1;  
            sh->free -= 1;  
            break;  
        }  
        f++;  
    }  
    va_end(ap);  
  
    /* Add null-term */  
    s[i] = '\0';  
    return s;  
}  
</span>


<span style="font-size:18px;">//将给定的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;//指向sds的头部
    ep = end = s+sdslen(s)-1;//指向sds的最后一个字符
    while(sp <= end && strchr(cset, *sp)) sp++;// 从sds的左边开始跳过所有连续在cset存在的字符
    while(ep > start && strchr(cset, *ep)) ep--;//从sds的右边开始跳过所有连续在cset中存在的字符
    len = (sp > ep) ? 0 : ((ep-sp)+1);//如此计算剩余的字符数:两个指针相减。如果左右移动已经交叉,则剩余空
    if (sh->buf != sp) memmove(sh->buf, sp, len);//更新buf
    sh->buf[len] = '\0';
    sh->free = sh->free+(int)(sh->len-len);
    sh->len = (int)len;
    return s;
}</span>


小结:

对比C字符串,sds有以下特性:

* 可以高效的执行长度计算

* 可以高效的执行追加操作

* 二进制安全

sds会为追加操作进行优化: 加快追加操作的速度,并降低内存分配次数,代价是多占用了内存空间(buf的free预留空间),而且这些内存不会被主动释放。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值