Redis源码阅读[1]: sdshdr

阅读Redis源码,从Redis的数据结构开始。sdshdr
Redis并没有使用C语言原生字符串,而是使用SDS(简单动态字符串),阅读源码来理解Redis作者是怎设计SDS,来处理我们平时使用C字符串所碰到的问题。

一、SDS定义

struct sdshdr {
  int len;   //buf已占用的空间长度
  int free; //buf中剩余的空间长度
  char but[];  //数据 真实存储c字符串
}

二、SDS与C字符串的区别

c语言的字符串很简单,用N+1的长度来标示长度为N的字符串

char cRedis[6] = "Redis";

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

A.避免缓冲区溢出

c语言的拼接字符串方法

char *strcat(char *dest, const char *src)

该函数需要程序员去确认dest是否有足够的空间容纳下src的内容,如果没办法容纳,则会造成溢出

而SDS在拼接字符串的时候,会杜绝溢出可能.

  • SDS调用拼接函数,构造出拼接字符串的长度
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t)):
}
  • SDS在拼接开始,调用sdsMakeRoomFor来决定是否进行扩容
sds makeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s); //获取s目前的剩余空间
    if (free > addlen) return s; //无需扩展

    size_t len, newlen;
    len = sdslen(s);
    sh = (void*)(s-sizeof(struct sdshdr))):

    newlen = (len + adulen);
    //redis扩容规则
    if  (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else 
        newlen += SDS_MAX_PERALLOC

    newsh = zrelloc(sh, sizeof(struct sdshdr) + newlen + 1);
    if  (news == NULL) return NULL;
    newsh->free = newlen - len;
    return newsh->buf;
}
  • SDS拼接函数
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);//赋值t中的内容到字符串尾部
    sh->len = curlen + len;
    sh->free = sh->free - len;
    s[curlen+len] = '\0';
    return s;
}
B、减少修改字符串带来的内存重新分配

C字符串的底层是一个定长的数组,每一次对字符串增长或者缩短的时候,都要进行内存重新分配

  • 增长C字符串,比如拼接,需要重新分配底层数据空间大小,如果忘记,则会造成溢出。
  • 缩短字符串,比如截断,需要释放不再使用的空间,否则造成内存泄露。

Redis数据库使用中,数据被频繁修改,C字符串会带来的频繁内存重分配,降低写速度,并带来性能问题. 而Redis的sdshdr解除了字符串长度和底层数组之间的长度关联,并通过空间预分配和惰性空间释放来减少内存重分配次数,提高性能

  • 空间预分配:扩容过程中,如果需求长度小于1MB,则double长度,如果大约1MB,则需求长度+1MB.通过此策略,减少内存重分配次数。
 //redis扩容规则
   if  (newlen < SDS_MAX_PREALLOC)
       newlen *= 2;
   else 
       newlen += SDS_MAX_PERALLOC
  • 惰性空间释放:当修改并缩短sds字符串,sds并不回收多出来的字节,而是仅仅通过free属性来记录可以使用的空间。以sdstrim代码为例:
sds sdstrim(sds s, const char *cset) {
    struct sdshdr *sh = (void*)(s- sizeof(struct sdshdr));
    char *start, *end, *sp, *ep;
    sp = start = s;
    ep = end = s + sdslen(s) + 1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > start && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1); //计算trim后的字符串长度
    if  (sh->buf != sp) memmove(sh->buf, sp, len);
    sp->buf[len] = '\0';
    sh->free = sh->free + (sh->len -len);
    sh->len = len;
    return s;
}
C.获取字符串长度复杂度为O(1),而C字符串为O(N);清理字符串,因为是惰性释放,时间复杂度也是O(1)
//获取字符串长度
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s- sizeof(struct sdshdr));
    return sh->len;
}
//清理字符串
void  sdsclear(sds s) {
    struct sdshdr *sh = (void*)(s-sizeof(struct sdshdr));
    sh->free += sh->len;
    sh->buf[0] = '\0';
}
D.兼容C字符串的一些特性。
  • 对比sdshdr字符串与C字符串是否一致
strcasecmp(sdshdr->buf, "hello world");
  • 添加sdshdr保存的字符串到C字符串之后
strcat(c_string, sds->buf);

三、SDS里面的其他方法

  • 把数组根据分隔符拼接成字符串
sds sdsjoin(char **argv, int argc, char *sep) {
    sds join = sdsempty();
    int j;
    for (j = 0; j < argc, j ++) {
        join = sdscat(join,argv[j]);
        if(j != argc -1) join = sdscat(join,sep);
     }
    return join;
}
  • sds字符串格式化
/**
  * %s - C String
 * %S - SDS string
 * %i - signed int
 * %I - 64 bit signed integer (long long, int64_t)
 * %u - unsigned int
 * %U - 64 bit unsigned integer (unsigned long long, uint64_t)
 * %% - Verbatim "%" character.
**/
sds sdscatfmt(sds s, char const *fmt,...) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    size_t initlen = sdslen(s);
    const char *f = fat;
    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;
        size_t 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*);
                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;
}
  • 复制字符串
sds sdscpy(sds s, const char *t, size_t len) {
    struct sdshdr *sh = (void*)(s- sizeof(struct sdshdr));
    size_t totlen = sh->free + sh->len; //buf现有长度
    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 = len;
    sh->free = totlen -len;
    return s;
}
  • 截取字符串
void sdsrange(sds s, int start, int end) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    size_t newlen, len = sdslen(s);

    if (len == 0) return;
    if (start < 0) {
        start = len+start;
        if (start < 0) start = 0;
    }
    if (end < 0) {
        end = len+end;
        if (end < 0) end = 0;
    }
    newlen = (start > end) ? 0 : (end-start)+1;
    if (newlen != 0) {
        if (start >= (signed)len) {
            newlen = 0;
        } else if (end >= (signed)len) {
            end = len-1;
            newlen = (start > end) ? 0 : (end-start)+1;
        }
    } else {
        start = 0;
    }
    if (start && newlen) memmove(sh->buf, sh->buf+start, newlen);    // 如果有需要,对字符串进行移动
    sh->buf[newlen] = 0;
    sh->free = sh->free+(sh->len-newlen);
    sh->len = newlen;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容