Redis源码学习:SDS设计与内存管理

为什么Redis选择SDS

1、缓解C语言字符串的缺陷

在 C 语言中可以使用 char* 字符数组来实现字符串。每个字符串分配一段连续的内存空间,依次存放字符串中的每一个字符,最后以null字符结尾。这种设计存在以下问题:
image.png

1、低效的操作
每次获取字符串长度都需要遍历字符串,找到null字符的位置,时间复杂度为O(n)。

2、二进制不安全
C字符串不能存储二进制数据,假如要存储的二进制图片中包含了null字符,会把它看做是字符串的结尾,出现错误。

3、缓冲区溢出
字符串操作(如拼接、复制)容易导致缓冲区溢出,进而引发安全问题。

2、提升性能和灵活性

SDS设计解决了传统C字符串存在的问题,提供了更高效、更安全的字符串操作能力,主要体现在以下几个方面:

1) 获取字符串长度的高效性

传统的C字符串需要线性遍历来获取长度,时间复杂度为O(n)。而SDS在结构体中维护了len字段,可以在常数时间O(1)内直接获取字符串长度,大大提升了效率。

2) 防止缓冲区溢出的安全性

C字符串在拼接、复制等操作时,很容易出现缓冲区溢出的问题,从而导致安全隐患。SDS则通过动态分配内存的方式,在字符串操作时自动调整内存大小,从根本上避免了缓冲区溢出。

3) 存储二进制数据的灵活性

传统C字符串由于使用null字符作为结尾标记,无法安全地存储包含null字符的二进制数据。而SDS则完全绕过了这一限制,可以方便地存储任意二进制数据,提高了数据存储的灵活性。

4) 空间预分配的优化

在对SDS进行扩展时,SDS会预先分配更大的内存空间,减少了频繁重新分配内存所带来的性能开销。当字符串长度小于1MB时,扩容后的空间是原长度的两倍;当长度超过1MB时,则会额外分配1MB的空间,防止过度使用内存。

SDS结构

在Redis源码中,SDS结构源码(位于sds.h文件)如下:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

定义了sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64多种结构,sdshdr5不再使用无需关注。每一个SDS结构都包含了实际存储字符串的字符数组buf[]记录当前字符串的长度len记录分配的总空间alloc(不包括头部和空终止符)SDS类型flags

它们的主要区别就在于,数据结构中的字符数组现有长度 len 和分配空间长度 alloc,这两个元数据的数据类型不同。uint8_t 是 8 位无符号整型,会占用 1 字节的内存空间。当字符串类型是sdshdr8时,它表示字符数组buf[]长度(包括数组最后一位\0)不会超过 256 字节(2 的 8 次方等于 256)。uint16_t、uint32_t、uint64_t,分别表示不超过 2 的 16 次方、32 次方和 64 次方。这两个元数据各自占用的内存空间在 sdshdr16、sdshdr32、sdshdr64 类型中,则分别是 2 字节、4 字节和 8 字节。

在SDS结构的定义上还使用了__attribute__ ((__packed__)),来优化空间,它的作用就是告诉编译器,在编译结构时,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。

image.png

SDS的内存分配过程

SDS的内存分配包括创建、扩展和释放三个主要过程。以下是SDS的内存管理源码分析。

1、SDS创建

SDS的创建函数是sdsnewlen

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    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;
    int hdrlen = sdsHdrSize(type);
    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);
    s = (char*)sh+hdrlen;
    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;
}
1.1 确定类型和头部大小

首先,通过sdsReqType函数确定适合的SDS类型,然后计算头部大小。sdsReqType根据字符串长度选择合适的SDS类型,而sdsHdrSize返回相应类型的头部大小。

char type = sdsReqType(initlen);
int hdrlen = sdsHdrSize(type);
1.2. 分配内存

接着,通过s_trymalloc_usables_malloc_usable函数分配内存,这里&usable会返回实际可用的内存大小。

sh = trymalloc?
	s_trymalloc_usable(hdrlen+initlen+1, &usable) :
	s_malloc_usable(hdrlen+initlen+1, &usable);
1.3. 初始化内存

根据init参数的不同,初始化内存。

if (init==SDS_NOINIT)
	init = NULL;
else if (!init)
	memset(sh, 0, hdrlen+initlen+1);
1.4. 设置类型和长度

最后,设置SDS的类型标志和长度,并返回字符串的指针。s表示是SDS中buf[]的起始位置,len表示使用空间,alloc表示分配的空用空间。

s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;

2、SDS扩容

SDS的扩展函数是sdsMakeRoomFor

sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    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 */
    if (greedy == 1) {
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            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;

    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;
}
2.1 计算可用空间

首先,计算当前字符串的可用空间和长度。

size_t avail = sdsavail(s);
size_t len = sdslen(s);
2.2 计算新长度

如果可用空间不足,则需要扩展。新长度通常是当前长度的两倍,以减少频繁的内存分配操作。SDS_MAX_PREALLOC是1M的大小,扩容后新空间的大小有两种情况:

  • 如果新长度小于1M,则新空间为扩容后字符串长度的两倍+1
  • 否则,则新空间为扩容后字符串长度+1M+1
    后面的+1是调用内存分配函数是添加的。
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
else
    newlen += SDS_MAX_PREALLOC;
2.3. 分配新内存

根据新长度分配内存。如果新的类型与旧的类型相同,直接使用s_realloc_usable重新分配内存,否则需要创建新的SDS,重新分配内存拷贝数据。

type = sdsReqType(newlen);
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
	newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
	if (newsh == NULL) return NULL;
	s = (char*)newsh+hdrlen;
} else {
	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);
}
2.4. 更新长度

最后,更新分配的总空间,并返回扩展后的SDS。

sdssetalloc(s, usable);
return s;

3、SDS的释放

SDS的释放函数是sdsfree

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}
3.1. 检查空指针

首先,检查传入的指针是否为空。

if (s == NULL) return;
3.2. 释放内存

通过s_free函数释放内存,注意需要将指针偏移到SDS头部的位置。

s_free((char*)s-sdsHdrSize(s[-1]));

总结

Redis使用简单动态字符串SDS来代替传统C字符串,解决了获取长度低效、缓冲区溢出和二进制不安全等问题。SDS在结构体中维护长度,支持二进制存储,自动扩容防止溢出,性能和灵活性均有提升。文中详细分析了SDS的内存分配、扩容和释放过程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小邓的技术笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值