Redis设计与实现 笔记 第二章 简单动态字符串

SDS的定义

redis使用自定义字符串来替代c语言传统的字符串,

struct sdshdr {
    int len;     // 当前字符串长度
    int free;    // 当前未使用的长度
    char buf[];  // 字节数组,用于保存字符串,和 C 语言字符串一样用 '\0' 结尾
};

可以使用

printf("%s",s->buff)来输出

1: 降低长度获取复杂度.

通常 C 语言长度会使用 strlen() 来进行长度的计算,其原理为从开始指针往后寻找 ‘\0’ 字符来确定长度, 复杂度是 O(n) 实际上是进行了遍历,而redis使用的结构,其记录了 len 的长度,所以复杂度是 O(1) ,确保获取字符串长度的工作不会成为 redis的性能瓶颈.反复使用STRLEN命令也不会造成任何影响.

2: 杜绝缓冲区溢出

C 语言使用 strcat() 来进行字符串拼接,典型出现的问题是,当目标字符串分配的长度不够追加长度和源字符串长度时,会造成内存溢出, redis 中使用 sdscatsds() 来进行字符串拼接,在拼接时会进行长度检查,如果长度不够会进行额外的处理,生成长度适当的对象来存储



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)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}
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';
    return s;
}

sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

3: 减少内存分配次数

上述提到, C 语言字符字进行拼接时,会进行额外的分配,如果原字符串的长度不够,需要重新进行分配操作,在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的,但是 Redis 作为数据库,经常被用于速度要求严苛,数据被频繁修改的场合,如果每进行字符串长度修改就要重分配内存,会对性能造成影响.
所以 Redis 作出以下优化:

(1): 空间预分配:

当 sds 长度小于1mb时候(len值),额外分配一倍内存,也就是如果buff长度为13字节,那么未使用空间也将是13字节,也就是buf实际长度为 13byte+13byte+1byte = 27
当 sds 长度大于1mb时,额外分配 1mb 的内存, 也就是buff识记长度为 1mb+ 1mb + 1byte

(2): 惰性空间释放

当一个字符串进行字符删减时,多出来的字符也会存放至free中,以备在未来可能的需要,总的来说,无论是预分配还是释放规则,最终目的都是用空间来减少内存的申请和释放,也是用空间换时间的思路.

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;

    // 修剪, T = O(N^2)
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > start && strchr(cset, *ep)) ep--;

    // 计算 trim 完毕之后剩余的字符串长度
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    
    // 如果有需要,前移字符串内容
    // T = O(N)
    if (sh->buf != sp) memmove(sh->buf, sp, len);

    // 添加终结符
    sh->buf[len] = '\0';

    // 更新属性
    sh->free = sh->free+(sh->len-len);
    sh->len = len;

    // 返回修剪后的 sds
    return s;
}

当然如果确定某字符串不会改变时,调用下面函数来进行真正的释放,来节省空间

sds sdsRemoveFreeSpace(sds s) {
    struct sdshdr *sh;

    sh = (void*) (s-(sizeof(struct sdshdr)));

    // 进行内存重分配,让 buf 的长度仅仅足够保存字符串内容
    // T = O(N)
    sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);

    // 空余空间为 0
    sh->free = 0;

    return sh->buf;
}

4: 二进制数据存储

C 语言字符串不能存放二进制数据,如图片,视频等,原因为如果图片视频中,带有’\0’时,会被截断处理,但 Redis 作为数据库显然是要考虑图片视频等存储功能的,所以当我们使用 len 来决定长度时,就可以解决该问题,而不会出现意外而发生截断的情况.

5: 兼容 C 字符串函数

虽然 Redis 是使用长度来决定字符串的长度,但是每个字符串后还是会用’\0’来进行结尾,也就是意味着, strcat(c_string,s->buff);第二个参数可以直接使用 sds 的成员来进行调用.

总结

C 字符串SDS
获取字符串长度复杂度为O(N)获取字符串长度复杂度为O(1)
API是不安全的,会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存分配修改字符串长度N次最多需要执行N次内存分配
只能保存文本内容可以保存文本内容,图片,以及视频
可以使用<string.h>中所有的函数可以使用一部分<string.h>中的函数

Redis 只会使用 C 字符串作为字面量,大多数情况下会使用 sds 来表示字符串

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值