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 选择不同的内存分配函数(zmalloczcalloc)来分配内存。然后设置 lenfree 字段,并在有初始化内容时将其复制到 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,然后返回 shlen 字段值,由于是内联函数,提高了多次调用时的效率。

  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 中的内容,只是修改了 lenfree 属性。

  1. sdscat 函数:将给定的 C 字符串拼接到 SDS 字符串的末尾。

    sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
    }

该函数调用 sdscatlen 函数,并传入 stt 的长度。

复制代码
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 的后部,并更新 lenfree 属性,最后添加结尾符号并返回新的 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 函数先检查 sbuf 长度是否足够,不足则调用 sdsMakeRoomFor 函数扩展空间,然后复制 t 的内容到 s 中,更新 lenfree 属性并返回新的 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 填充,并更新 lenfree 属性。

  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;
    }

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

相关推荐
一只自律的鸡36 分钟前
【MySQL】第二章 基本的SELECT语句
数据库·mysql
liliangcsdn2 小时前
如何使用python创建和维护sqlite3数据库
数据库·sqlite
TDengine (老段)8 小时前
TDengine 数学函数 DEGRESS 用户手册
大数据·数据库·sql·物联网·时序数据库·iot·tdengine
TDengine (老段)8 小时前
TDengine 数学函数 GREATEST 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
安当加密8 小时前
云原生时代的数据库字段加密:在微服务与 Kubernetes 中实现合规与敏捷的统一
数据库·微服务·云原生
爱喝白开水a9 小时前
LangChain 基础系列之 Prompt 工程详解:从设计原理到实战模板_langchain prompt
开发语言·数据库·人工智能·python·langchain·prompt·知识图谱
想ai抽9 小时前
深入starrocks-多列联合统计一致性探查与策略(YY一下)
java·数据库·数据仓库
武子康9 小时前
Java-152 深入浅出 MongoDB 索引详解 从 MongoDB B-树 到 MySQL B+树 索引机制、数据结构与应用场景的全面对比分析
java·开发语言·数据库·sql·mongodb·性能优化·nosql
longgyy9 小时前
5 分钟用火山引擎 DeepSeek 调用大模型生成小红书文案
java·数据库·火山引擎
ytttr87310 小时前
C# 仿QQ聊天功能实现 (SQL Server数据库)
数据库·oracle·c#