Redis源码阅读笔记(1)-简单动态字符串SDS

字符串是Redis中一个重要的组成部分,Redis没有直接使用C语言自带的字符串,而是自身构建了一个简单动态字符串(Simple dynamic string, SDS)的抽象类型,该抽象类型不仅有额外的特性,还能兼容部分C语言内建的字符串操作函数。

涉及的主要源代码文件

  1. sds.h
  2. sds.c

SDS的定义

typedef char *sds;      //声明一个字符串指针类型的别名

//动态字符串结构
//总长度 = len + free + 1  (1是末尾字符串\0所占的字节)
struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];        //flexible array member,在计算struct长度是不算在内
};
SDS示例

SDS字符串与C字符串相比,在结构体中定义lenfree属性,len用来记录当前buf数组中已使用的字节空间,free记录了buf数组中未使用的字符串空间。SDS字符串与C字符串一样,使用了\0作为字符串结尾,但给\0字符不算入len中,因此buf数组的总大小应为total = len + free + 1。如图中Redis的SDS字符串buf分配的空间为10字节。\0字节的添加完全有SDS底层函数负责,使用者无需关心,也由于这个\0字符的存在,使得SDS可以重用一部分C字符串函数。

备注:sizeof(sdshdr) = 2 * sizeof(int64),buf所占的空间为0,可参考flexible array member

SDS的关键实现细节

//得到动态字符串锁保存字符串的长度(不含\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;
}

由于SDS中记录了自身的长度len,因此获取字符串长度的时间复杂度为O(1),而不是C字符串的O(N),因此提高了获取字符串长度的性能。从上面可以看到,lenfree的获取需要通过指针计算来获取sdshdr的实际地址来获得的。


//指定长度初始化sds,init表示初始化填写的内容,init为空表示初始化字符串长度为0
sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    //sdshdr的长度: strlen(sdshdr) + initlen + 1(1表示末尾0所占的资费)
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);      
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);      //初始化为0
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);                     //复制init指向地址的字符串
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;                                  //sds的指针为实际字符串的指针
}

sdsnewlen的返回可以看到,实际返回给上层的是buf数组的地址,对上层屏蔽了sdshdrsdshdr的地址可以通过sdshder.buf的地址来算出。


//增加sds的额外可存储空间至addlen字节,free = addlen
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;    //当free空间足够时,直接返回,无需重分配
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)      //预分配策略,若新字符长度小于1M,则为字符串分配2倍所需空间的大小
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;     //否则,只额外添加1M未使用空间
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}

//字符串左右修剪函数
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;
    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);
    if (sh->buf != sp) memmove(sh->buf, sp, len); //移动内存存储内容
    //更新末尾、len、free
    sh->buf[len] = '\0';
    sh->free = sh->free+(sh->len-len);
    sh->len = len;
    return s;
}

//获取子字符串,start/end 允许传负数
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;
}

由于SDS字符串存在未使用空间,因此修改字符串长度不像C字符串,无需频繁的通过内存重分配来扩展或缩小字符串所占大小。这对于需要频繁修改数据的Redis是一个极大的性能提升sdsMakeRoomFor函数用来对SDS字符串进行扩展,sdstrimsdsrange用来对字符串进行缩减。

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

  1. 空间预分配
    若对字符串进行扩展后的大小(newlen)小于1M (1024*1024字节),那么给SDS字符串额外分配所需大小一倍(扩展后大小为2*newlen)的空间。若对字符串进行扩展后的大小大于1M,则给SDS字符串额外分配1M空间。通过这种方式,减少了执行字符串增长操作所需的内存分配次数。

  2. 惰性释放
    当SDS字符串进行缩减时,并不释放多出来的空间,而是通过修改free属性和len属性来表示字符串的缩减,宠儿频繁的内存操作。


//连接指定长度字符串到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);
    sh->len = curlen+len;
    sh->free = sh->free-len;
    s[curlen+len] = '\0';           //末尾添加0
    return s;
}

SDS不使用C字符串函数的strcat函数,而是自己封装了一个,当free空间不足时,会扩展SDS字符串的未使用空间,然后在进行拼接,从而避免了缓冲区溢出

总结

SDS字符串相比C字符串的优势:

  1. O(1)获取字符串长度;
  2. 通过空间预分配和惰性空间释放减少内存的操作次数;
  3. 杜绝缓冲区溢出
  4. 因为是通过len来控制字符串的长度,不依赖于\0,因此字符串是二进制安全的,不仅可以保存文本数据,还可以用来保存任意格式的二进制数据。
  5. 因为SDS字符串末尾带有\0,因此作为存储普通字符串,可以使用部分C语言字符串函数。
SDS主要API
function description time complexity
sdslen 获取sds字符串长度 O(1)
sdsavail 获取sds可用长度 O(1)
sdsnewlen 创建指定长度的sds,并接受C字符串为初始化内容 O(N)
sdsnew 根据给定的C字符串创建sds O(N)
sdsempty 创建一个空的sds O(1)
sdsdup 复制sds O(N)
sdsfree 释放sds O(1)
sdsgrowzero 将sds扩展到指定长度,新扩展的内容用\0赋值 O(N)
sdscatlen 将一个给定字符串复制指定长度到sds末尾 O(N)
sdscat 将一个给定字符串添加到sds末尾 O(n)
sdscatsds 将一个sds添加到sds末尾 O(N)
sdscpylen 将一个给定字符串复制一定长度到sds中 O(N)
sdscpy 将一个给定字符串复制到sds中 O(N)
sdstrim 修剪sds O(M*N),M为sds长度,N为修剪内容长度
sdsrange 保留给定sds一定范围的长度 O(N)
sdsupdatelen 重新计算sds文本字符串的长度 O(N)
sdsclear 清除sds为空字符串 O(1)
sdscmp 比较sds字符串 O(N)
sdstolower sds字符串转为小写 O(N)
sdstoupper sds字符串转为大写 O(N)
Reference
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容