Redis 简单动态字符串和传统字符串对比(二)

60 阅读3分钟

1、C 字符串不记录自身长度,造成缓冲区溢出场景

  • 例如:在程序中有两个内存相邻的 C 字符串 s1 和 s2 , 其中 s1 保存了字符串 "Redis",s2 保存了字符串 "MongoDB",如图所示

  • 使用 strcat(s1,"Cluster"); 将 "Cluster" 拼接到 s1 中,由于 s1 没有分配足够的空间,s1 的数据将溢出到 s2 所在的空间中,导致 s2 保存的内容被意外修改,如图所示

#include<stdio.h>
int main() {
    // 传统C字符串保存形式: 'H''e''l''l''o''''a''a''a''a''a''\0'
    char str1[12] = "Hello aaaaa";
    char str2[6] = "world";

    printf("Before merging: str1 = %s, str2 = %s\n", str1, str2);

    // Merge str2 into str1
    // strcat(str1, str2);
    // printf("After merging: str1 = %s, str2 = %s\n", str1, str2);

    return 0;
}

2、SDS 是如何避免缓冲区溢出的?

1. 空间预分配

SDS API 对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足修改所需要的要求,如果不满足的话,API 会自动将 SDS 空间扩展至执行所需要的大小,然后再执行实际的修改操作;

分配规则:

  • SDS 修改之后长度 < 1 MB:Redis 先分配数据实际需要的空间,同时还会分配和 len 属性同样大小的未使用空间(减少内存重分配次数),此时 SDS buf 数组的实际长度 = 未使用空间 + 已使用空间 +1,如下图:

修改前:

修改后:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义SDS结构
typedef struct sds {
    // 字符串的长度
    int len;

    // 剩余的未使用空间
    int free;

    // 字符串缓冲区,使用柔性数组
    char buf[];
} sds;

// 初始化 SDS 字符串
sds* sdsnew(const char* init) {
    // 初始化字符长度
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    sds* s = malloc(sizeof(sds) + initlen + 1);
    if (s == NULL) return NULL;

    // 给 len 赋值 = 当前字符串长度
    s->len = initlen;

    // 给 free 赋予初始值 0
    s->free = 0;

    if (initlen) memcpy(s->buf, init, initlen);
    s->buf[initlen] = '\0';
    return s;
}

// 拼接字符串到现有的SDS字符串
sds* sdscat(sds* s, const char* t) {
    // 原字符串长度
    size_t totlen, curlen = s->len;
    // 拼接的字符串长度
    size_t tlen = strlen(t);

    if (s->free < tlen) {
        // free 空间不够,需要重新分配空间
        totlen = curlen + tlen;
        s = realloc(s, sizeof(sds) + totlen + 1);
        // 设置 free 为新的总长度
        s->free = totlen;
    }

    memcpy(s->buf + curlen, t, tlen);
    s->len = curlen + tlen;
    s->buf[s->len] = '\0';
    return s;
}

int main() {
    // 创建一个新的SDS字符串
    sds* mystring = sdsnew("Hello");

    // 获取SDS字符串的长度
    printf("旧字符串 len 值: %d\n", mystring->len);

    // 获取SDS字符串的当前分配空间
    printf("SDS 总分配空间: %zu\n", mystring->len + mystring->free + 1);

    // 计算剩余未使用空间
    printf("剩余未使用空间: %d\n", mystring->free);

    printf("------------------- \n");

    // 拼接字符串
    mystring = sdscat(mystring, "World");

    // 获取SDS字符串的长度
    printf("拼接新字符串后 len 值: %d\n", mystring->len);

    // 获取SDS字符串的当前分配空间
    printf("拼接新字符串后 SDS 总分配空间: %zu\n", mystring->len + mystring->free + 1);

    // 计算剩余未使用空间
    printf("拼接新字符串后 SDS 剩余未使用空间: %d\n", mystring->free);

    // 释放SDS字符串的内存空间
    free(mystring);

    return 0;
}
  • SDS 修改后的长度 >= 1MB 那么修改之后的 len 将变成 30 MB,同时分配 1 MB 的未使用空间,SDS 的实际长度 = 30 MB +1 MB + 1 byte;

2. 惰性空间释放

  • 惰性空间释放用于优化 SDS 的字符串缩短操作:当缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。