Redis 中简单动态字符串(SDS)的深入解析

在 Redis 中,简单动态字符串(Simple Dynamic String,SDS)是一种非常重要的数据结构,它在 Redis 的底层实现中扮演着关键角色。本文将详细介绍 SDS 的结构、Redis 使用 SDS 的原因以及 SDS 的主要 API 及其源码解析。

一、SDS 简介

SDS 是 Redis 默认的字符表示,用于保存数据库中的字符串值。它不仅可以存储文本数据,还能存储任意格式的二进制数据,如图片、视频等。同时,SDS 还被用作缓冲区,例如 AOF 模块的 AOF 缓冲区以及客户端状态中的输入缓冲区。

二、SDS 结构

SDS 的结构定义如下:

struct sdshdr {
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 字节数组
    char buf[];
};

在这个结构中,len 记录了 buf 数组中已使用的字节数,也就是当前 SDS 所保存字符串的长度;free 记录了 buf 数组中未使用的字节数;buf 是一个柔性数组,用于实际保存字符串内容。例如,当 free = 5 时,表示空闲空间长度为 5;len = 5 时,表示已经使用的空间长度为 5。

三、Redis 使用 SDS 的原因

  1. 常数复杂度获取字符串长度:获取 SDS 字符串长度的操作时间复杂度为 \(O(1)\),因为 len 字段已经记录了字符串的长度。而传统 C 字符串获取长度需要遍历整个字符串,时间复杂度为 \(O(N)\),使用 SDS 可以确保获取字符串长度的操作不会成为 Redis 的性能瓶颈。
  2. 杜绝缓冲区溢出:C 字符串不记录自身长度和空闲空间,在进行字符串拼接等操作时容易造成缓冲区溢出。而 SDS 在拼接字符串之前会先通过 free 字段检测剩余空间能否满足需求,如果不足则会进行扩容,从而避免了缓冲区溢出的问题。
  3. 减少修改字符串时的内存重分配次数
    • 空间预分配:在对 SDS 进行扩展时,程序不仅会为 SDS 分配修改所必须的空间,还会分配额外的未使用空间。这样可以减少连续执行字符串增长操作所需的内存重分配次数,将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。
    • 惰性空间释放:在对 SDS 进行缩短操作时,程序不会立刻使用内存重分配来回收缩短之后多出来的字节,而是通过 free 属性将这些字节的数量记录下来,等待将来使用。这避免了缩短字符串时所需的内存重分配次数,并且为将来可能的增长操作提供了优化。
  4. 二进制安全:SDS 的 API 都是二进制安全的,所有 API 都会以处理二进制的方式来处理存放在 buf 数组里的数据,程序不会对其中的数据做任何限制、过滤,数据存进去是什么样子,读出来就是什么样子。因此 Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据。

四、SDS 主要 API 及其源码解析

  1. sdsnew 函数:用于创建一个包含给定字符串的 SDS。
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

该函数首先判断 init 是否为 NULL,如果是则将 initlen 设为 0,否则计算 init 所指向字符串的长度。然后调用 sdsnewlen 函数来创建 SDS。

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
    // 根据是否有初始化内容,选择适当的内存分配方式
    if (init) {
        // zmalloc 不初始化所分配的内存
        sh = zmalloc(sizeof(struct sdshdr) + initlen + 1);
    } else {
        // zcalloc 将分配的内存全部初始化为 0
        sh = zcalloc(sizeof(struct sdshdr) + initlen + 1);
    }
    // 内存分配失败,返回
    if (sh == NULL) return NULL;
    // 设置初始化长度
    sh->len = initlen;
    // 新 sds 不预留任何空间
    sh->free = 0;
    // 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    // 以 \0 结尾
    sh->buf[initlen] = '\0';
    // 返回 buf 部分,而不是整个 sdshdr,因为 sds 是 char 指针类型的别名
    return (char*)sh->buf;
}

sdsnewlen 函数根据 init 是否为 NULL 选择不同的内存分配函数(zmalloc 或 zcalloc)来分配内存。然后设置 len 和 free 字段,并在有初始化内容时将其复制到 buf 中,最后返回 buf 指针。

  1. sdsempty 函数:创建一个不包含任何内容的 SDS。
sds sdsempty(void) {
    return sdsnewlen("", 0);
}

该函数简单地调用 sdsnewlen 函数,传入空字符串和长度 0 来创建一个空的 SDS。

  1. sdsfree 函数:释放给定的 SDS。
void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s - sizeof(struct sdshdr));
}

由于 s 指向的是 buf 数组的起始位置,而内存分配时是分配了 struct sdshdr 结构体和 buf 数组的连续空间,所以通过 s - sizeof(struct sdshdr) 得到指向 struct sdshdr 起始位置的指针,然后调用 zfree 函数释放内存。

  1. sdslen 函数:返回 SDS 的已使用的空间字节数。
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    return sh->len;
}

该函数通过 s - sizeof(struct sdshdr) 获取指向 struct sdshdr 结构体的指针 sh,然后返回 sh 的 len 字段值,由于是内联函数,提高了多次调用时的效率。

  1. sdsavail 函数:返回 SDS 的未使用的空间字节数。
static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    return sh->free;
}

与 sdslen 函数类似,通过获取 struct sdshdr 结构体指针 sh,返回其 free 字段值。

  1. sdsdup 函数:创建一个给定 SDS 的副本。
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

该函数调用 sdsnewlen 函数和 sdslen 函数,根据传入的 SDS s 的内容和长度创建一个新的 SDS 副本。

  1. sdsclear 函数:清空 SDS 保存的字符串内容。
void sdsclear(sds s) {
    // 取出 sdshdr
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    // 重新计算属性
    sh->free += sh->len;
    sh->len = 0;
    // 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
    sh->buf[0] = '\0';
}

该函数采用惰性空间释放策略,将 free 增加 len 的值,将 len 设为 0,并将 buf 的第一个字符设为 \0,实际上并没有真正删除 buf 中的内容,只是修改了 len 和 free 属性。

  1. sdscat 函数:将给定的 C 字符串拼接到 SDS 字符串的末尾。
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

该函数调用 sdscatlen 函数,并传入 st 和 t 的长度。

sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;
    // 原有字符串长度
    size_t curlen = sdslen(s);
    // 扩展 sds 空间
    s = sdsMakeRoomFor(s, len);
    // 内存不足?直接返回
    if (s == NULL) return NULL;
    // 复制 t 中的内容到字符串后部
    sh = (void*)(s - (sizeof(struct sdshdr)));
    memcpy(s + curlen, t, len);
    // 更新属性
    sh->len = curlen + len;
    sh->free = sh->free - len;
    // 添加新结尾符号
    s[curlen + len] = '\0';
    // 返回新 sds
    return s;
}

sdscatlen 函数先调用 sdsMakeRoomFor 函数扩展 SDS 空间,然后将 t 的内容复制到 s 的后部,并更新 len 和 free 属性,最后添加结尾符号并返回新的 SDS。

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);
    size_t len, newlen;
    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) return s;
    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*)(s - (sizeof(struct sdshdr)));
    // s 最少需要的长度
    newlen = (len + addlen);
    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 最大预先分配长度
        // 那么为它分配两倍于所需长度的空间 空间预分配策略
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + 1);
    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;
    // 更新 sds 的空余长度
    newsh->free = newlen - len;
    // 返回 sds
    return newsh->buf;
}

sdsMakeRoomFor 函数采用空间预分配策略,根据当前空余空间和需要增加的长度来决定分配的新空间大小,然后调用 zrealloc 函数重新分配内存并更新 free 属性。

  1. sdscatsds 函数:将给定的 SDS 字符串拼接到另一个 SDS 字符串的末尾。
sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

该函数调用 sdscatlen 函数和 sdslen 函数,将 t 拼接到 s 的末尾。

  1. sdscpy 函数:将给定的 C 字符串复制到 SDS 里面,覆盖 SDS 原有的字符串。
sds sdscpy(sds s, const char *t) {
    return sdscpylen(s, t, strlen(t));
}

该函数调用 sdscpylen 函数。

sds sdscpylen(sds s, const char *t, size_t len) {
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    // sds 现有 buf 的长度
    size_t totlen = sh->free + sh->len;
    // 如果 s 的 buf 长度不满足 len ,那么扩展它
    if (totlen < len) {
        // T = O(N)
        s = sdsMakeRoomFor(s, len - sh->len);
        //扩展失败,返回NULL
        if (s == NULL) return NULL;
        //扩展成功
        sh = (void*)(s - (sizeof(struct sdshdr)));
        totlen = sh->free + sh->len;
    }
    // 复制内容
    memcpy(s, t, len);
    // 添加终结符号
    s[len] = '\0';
    // 更新属性
    sh->len = len;
    sh->free = totlen - len;
    // 返回新的 sds
    return s;
}

sdscpylen 函数先检查 s 的 buf 长度是否足够,不足则调用 sdsMakeRoomFor 函数扩展空间,然后复制 t 的内容到 s 中,更新 len 和 free 属性并返回新的 SDS。

  1. sdsgrowzero 函数:用空字符将 SDS 扩展至给定长度。
sds sdsgrowzero(sds s, size_t len) {
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    size_t totlen, curlen = sh->len;
    // 如果 len 比字符串的现有长度小,
    // 那么直接返回,不做动作
    if (len <= curlen) return s;
    // 扩展 sds
    s = sdsMakeRoomFor(s, len - curlen);
    // 如果内存不足,直接返回
    if (s == NULL) return NULL;
    // 将新分配的空间用 0 填充,防止出现垃圾内容
    sh = (void*)(s - (sizeof(struct sdshdr)));
    memset(s + curlen, 0, (len - curlen + 1));
    // 更新属性
    totlen = sh->len + sh->free;
    sh->len = len;
    sh->free = totlen - sh->len;
    // 返回新的 sds
    return s;
}

该函数先判断 len 是否小于当前长度,小于则直接返回。否则调用 sdsMakeRoomFor 函数扩展空间,然后用 memset 函数将新分配的空间用 0 填充,并更新 len 和 free 属性。

  1. sdsrange 函数:保留 SDS 给定区间内的数据,不在区间内的数据会被覆盖或者清除。
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;
    //start参数规则
    if (start < 0) {
        start = len + start;
        if (start < 0) start = 0;
    }
    //end参数规则
    if (end < 0) {
        end = len + end;
        if (end < 0) end = 0;
    }
    //len取决于start和end的关系
    newlen = (start > end) ? 0 : (end - start) + 1;
    //新的sds的len!=0
    if (newlen != 0) {
        //需要截取的起点大于等于有符号的len 那么新的sds的len=0
        if (start >= (signed)len) {
            newlen = 0;
        }
        //终点超出了有符号的len 终点就是len-1
        else if (end >= (signed)len) {
            end = len - 1;
            //重新计算len
            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;
}

该函数先处理 start 和 end 参数,根据它们计算出新的长度 newlen,然后根据情况移动字符串内容,添加终结符并更新 len 和 free 属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值