当前位置: 首页 > news >正文

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 将分配的内存全部初始化为 0sh = 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) {// 取出 sdshdrstruct 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';// 返回新 sdsreturn 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_PREALLOCnewlen += SDS_MAX_PREALLOC;// T = O(N)newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + 1);// 内存不足,分配失败,返回if (newsh == NULL) return NULL;// 更新 sds 的空余长度newsh->free = newlen - len;// 返回 sdsreturn 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);//扩展失败,返回NULLif (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;// 返回新的 sdsreturn 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;// 扩展 sdss = 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;// 返回新的 sdsreturn 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!=0if (newlen != 0) {//需要截取的起点大于等于有符号的len 那么新的sds的len=0if (start >= (signed)len) {newlen = 0;}//终点超出了有符号的len 终点就是len-1else if (end >= (signed)len) {end = len - 1;//重新计算lennewlen = (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 属性。

相关文章:

  • 基于Redis实现优惠券秒杀——第3期(分布式锁-Redisson)
  • Java学习手册:Spring 多数据源配置与管理
  • 【题解-洛谷】B4303 [蓝桥杯青少年组省赛 2024] 字母移位
  • kotlin 02flow-sharedFlow 完整教程
  • PyCharm 安装教程
  • Docker(三):DockerFile
  • 从零开始学Flink:开启实时计算的魔法之旅
  • 【RocketMQ Broker 相关源码】-注册 broker 信息到所有的 NameServer
  • 【Spring Boot】Spring Boot + Thymeleaf搭建mvc项目
  • Kubernetes控制平面组件:Controller Manager 之 内置Controller详解
  • SpringBoot企业级开发之【文章列表(条件分页)】
  • 利用 Python pyttsx3实现文字转语音(TTS)
  • 如何使用QWidgets设计一个类似于Web Toast的控件?
  • js获取明天日期、Vue3大菠萝 Pinia的使用
  • Unity:Surface Effector 2D(表面效应器 2D)
  • C++入门(上)--《Hello C++ World!》(1)(C/C++)
  • 学习海康VisionMaster之亮度测量
  • 【Bootstrap V4系列】学习入门教程之 组件-按钮组(Button group)
  • Spring 框架的底层原理
  • linux 高并发 文件句柄数 fs 及 tcp端口数调优
  • 央行行长详解降息:将通过利率自律机制引导商业银行相应下调存款利率
  • 老人刷老年卡乘车被要求站着?公交公司致歉:涉事司机停职
  • 五角大楼要裁一批四星上将
  • 怎样正确看待体脂率数据?或许并不需要太“执着”
  • 山东如意集团及实控人等被出具警示函,此前实控人已被罚十年禁止入市
  • “五一”假期国内出游3.14亿人次,同比增长6.4%