Redis源码阅读02-数据结构sds

redis为什么不使用c语言对字符串的实现?

c语言的字符串表示有一下几个缺点:

  • c语言的字符串是以\0作为判断数组结束的标记,所以如果存储的字符串中包含\0的话,则无法获取全部的字符。
  • c语言字符串的获取字符串长度,只能将内容进行遍历后才能获取到长度,时间复杂度是O(n)。
  • c语言的字符串不记录长度,在拼接长度时可能造成内存溢出。

sds解决了以上问题

  • 拥有len属性记录字符串长度,获取长度的时间复杂度由O(n)变为直接获取len属性的O(1)
  • sds在拼接时首先会获取可用空间,并进行判断是否需要进行扩容,所以不会造成缓存区溢出,这点上比c语言的string是更安全的
  • sds在扩容时实现了空间预分配,申请内存时会多申请一部分内存出来,一定程度上避免频繁申请内存。

空间预分配

sds预空间分配的实现就在 sds.c的sdsMakeRoomFor方法中,首先通过sdsavail方法获取sds的剩余空间,然后判断剩余空间是否足够新增长度的使用,如果足够使用则直接返回,如果不足够使用,则判断(新增长度+已用长度)所需长度是否是小于1M,如果小于1M,则直接加倍。这种方式相当于预分配了一部分内存,

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    //获取sds剩余空间
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    /* Return ASAP if there is enough space left. */
    //判断字符串的剩剩余空间是否足够,如果足够直接返回
    if (avail >= addlen) return s;
    //获取字符串的原长度
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    //字符串的新长度 = 原长度 + 新增长度
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    //这里判断新长度是否超过了1M
    if (newlen < SDS_MAX_PREALLOC)
        //没超过直接申请所需长度的2倍
        newlen *= 2;
    else
        //超过了1M则申请newlen + 1M的长度
        newlen += SDS_MAX_PREALLOC;
    // 转换新长度的类型
    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    //根据新长度转换的类型获取hdr结构体的长度
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    //如果类型没有变更 
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

sds的指针位置

sds新建字符串的方法 sdsnew和sdsnewlen的实现方法,可以看到,s对应的指针,就是buf的所在位置,所以sds也可以直接当做string来使用,巧妙的指针使用也是让sds完美兼容string,另外因为sdshdr的定义是使用的紧凑式内存对齐(参考sds对齐),还可以通过buf的位置-1直接找到flags的位置。

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    //这里定义了sds的对象s
    sds s;
    //根据长度转换sds的type
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    //根据 type计算出sds sdshdr的长度。
    int hdrlen = sdsHdrSize(type);
    //定义flags的指针
    unsigned char *fp; /* flags pointer. */
    size_t usable;

    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
    //申请内存
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    //将sds的指针指向 sh+hdrlen,也就是buf数组的位置
    s = (char*)sh+hdrlen;
    //s-1的位置也就是 flags的位置
    fp = ((unsigned char*)s)-1;
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

free space 可用空间

free space可用空间(也有文章翻译叫惰性空间),一方面是指空间预分配以后除去占用的空间以外空余的空间,另一方面,在sds进行修改时,并不会将sds已经申请的buf[]空间的内存进行回收,仅是将sds的len值进行重新设置,已经申请的空间在那里留着以后再用。

static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

关于sds的最大长度为512mb

当客户端操作 client 时,一般不会直接使用 sds ,而是通过对象的方式来使用。比如创建的字符串其实是一个对象,间接使用到了 sds 结构。限制 512M 的逻辑在 t_string.c 的 checkStringLength 方法。

在redis3.2.13、redis4.0.14、redis5.0.9版本里面的的一个方法,checkStringLength里面写死了限制512*1024*1024,这个方法常在 SET/HSET的时候被调用。

static int checkStringLength(client *c, long long size) {
    if (size > 512*1024*1024) {
        addReplyError(c,"string exceeds maximum allowed size (512MB)");
        return C_ERR;
    }
    return C_OK;
}

而在redis6.2.7版本里面,这个长度已经改为读取配置 proto-max-bulk-len的长度了。

static int checkStringLength(client *c, long long size) {
    if (!(c->flags & CLIENT_MASTER) && size > server.proto_max_bulk_len) {
        addReplyError(c,"string exceeds maximum allowed size (proto-max-bulk-len)");
        return C_ERR;
    }
    return C_OK;
}

参考文档:

http://www.shutdown.cn/post/redis-read-code-02-about-sds/

https://www.cnblogs.com/sulishihupan/p/14512066.html

https://blog.csdn.net/wejack/article/details/127250866

https://blog.csdn.net/wejack/article/details/127694676

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值