sds.c/sds.h
一、 SDS的定义
SDS(Simple Dynamic String,简单动态字符串),是redis底层使用的字符串表示,取代了C语言中默认的char *类型。
与C语言的默认字符串相比,sds既可以高效地实现追加和长度计算,同时也是二进制安全的,并会在字符串末尾自动添加 '\0'。
采用默认的空字符结尾,使得sds可以兼容部分C字符串函数。
所以,在Redis中,C字符串只会作为字符串字面量,用在一些不需要对字符串值进行修改的地方。
在sds.h/sdshdr中,对SDS进行了定义:
typedef char *sds; //类型别名,用于指向 sdshdr 中的 buf 属性
struct sdshdr {
unsigned int len; //已使用的字节数
unsigned int free; //可用的字节数
char buf[]; //数据空间,实际保存字符串数据的地方
};
对于 sdshdr 来说,sizeof(struct sdshdr)= 2*sizeof(unsigned int),因为 char buf[] 等价于 char buf[0], 它仅对编译器有效,并不实际占用存储。
所以,一个sds字符串申请的实际内存为:sizeof(sdshdr)+len+free+1,其中的“1”实际上是sds自动添加的字符'\0'。
另外,sds 指向 sdshdr 中的 buf 属性,要是想获取 sdshdr 结构体指针,可以通过以下代码来获取:
struct sdshdr *sh = (void *)(s -(sizeof(struct sdshdr)));
二、 SDS API
函数 | 作用 | 时间复杂度 |
---|---|---|
sdslen | 返回 SDS 实际保存的字符串的字节数 | O(1) ,直接读取 SDS 的 len 属性 |
sdsavail | 返回 SDS 可用空间的字节数 | O(1) |
sdsnew | 创建并返回一个包含给定C字符串的 SDS ,新 SDS 不预留任何空间,即 free 属性值为0 | O(N),N为给定C字符串的长度 |
sdsempty | 创建并返回一个不包含任何东西的 SDS | O(1),直接读取 SDS 的 free 属性 |
sdsdup | 创建一个给定 SDS 的副本 | O(N),N为给定 SDS 的长度 |
sdsfree | 释放给定的 SDS | O(N),N为给定 SDS 的长度 |
sdsclear | 清空 SDS 保存的字符串内容,即将'\0'置于 sdshdr 的 buf[0] 处,惰性地删除 buf 中的内容 | O(1) |
sdsMakeRoomFor | 扩展 SDS 中 buf 的长度 | O(N) |
sdsRemoveFreeSpace | 回收 SDS 中的空闲空间 | O(N) |
sdsIncrLen | 调整 SDS 字符串:增加字符串长度,缩减空余空间,并将 '\0' 放到新字符串的末端。用于在调用 sdsMakeRoomFor() 函数对字符串进行扩展,用户在字符串末端写入某些内容之后,正确更新 free 和 len 属性 | O(1) |
sdsAllocSize | 返回给定 SDS 分配的内存字节数,包括 SDS 头部开销,已用字节数(len),可用字节数(free)和一字节的空字符 | O(1) |
sdsgrowzero | 用 0 字节将 SDS 扩充至指定长度 | O(N),N为扩展新增的字节数 |
sdscatlen | 将长度为 len 的给定C字符串追加到 SDS 字符串的末尾 | O(N) |
sdscat | 将给定C字符串追加到 SDS 字符串的末尾 | O(N) |
sdscatsds | 将给定 SDS 字符串追加到 SDS 字符串的末尾 | O(N) |
sdscpylen | 将给定C字符串的前 len 位复制到 SDS 字符串当中,覆盖 SDS 字符串原有内容,并在字符串的末尾添加 '\0' | O(N) |
sdscpy | 将字符串复制到 SDS 当中,覆盖原有内容 | O(N) |
sdsfromlonglong | 根据输入的 long long 类型的值,创建一个 SDS | O(N) |
sdsll2str | 将 long long 类型的值转换为字符串 | O(N) |
sdsull2str | 和 sdsll2str 函数功能相同,不过是用于 unsigned long long 类型的值 | O(N) |
sdscatvprintf | 打印函数,将格式化的字符串追加到 SDS 字符串后,内部使用了 vsnprintf() 函数 | O(N^2) |
sdscatprintf | 打印函数,功能同上,内部调用 sdscatvprintf 函数 | O(N^2) |
sdscatfmt | 功能与 sdscatprintf 函数类似,但是速度要快得多,因为这是 Redis 自己实现的 sprintf() 函数,而不是上面用的libc库的方法 | O(N) |
sdstrim | 接受一个 SDS 和一个 C 字符串作为参数,从 SDS 左右两端分别移除在给定 C 字符串中出现过的字符 | O(M*N),M 为 SDS 长度, N 为给定的 C 字符串的长度 |
sdsrange | 保留 SDS 给定区间内的数据,不在区间内的数据将会被覆盖或清除。给定区间的索引可以为负数,且 -1 代表着最后一个字符, -2 代表着倒数第二个字符 | O(N),N为被保留数据的字节数 |
sdstolower | 将 SDS 字符串中的所有字符转换为小写 | O(N) |
sdstoupper | 将 SDS 字符串中的所有字符转换为大写 | O(N) |
sdscmp | 对比两个 SDS 字符串是否相同 | O(N),N为比较的两个字符串中较短的一个的长度 |
sdssplitlen | 使用分隔符对 SDS 字符串进行分割,并返回分割后的 SDS 数组。并且该函数是二进制安全的 | O(n^2) |
sdsfreesplitres | 释放 sdssplitlen() 函数产生的 SDS 数组 | O(N^2) |
sdscatrepr | 将给定的字符串以带引号的方式追加到给定 SDS 字符串的末尾 | O(N) |
sdssplitargs | 将一行文本分割成多个参数。这个函数主要用于 config.c 中对配置文件进行分析 | O(N^2) |
sdsmapchars | 对 SDS 字符串做字符替换,将 from 中出现的字符替换为 to 中对应位置上的字符。比如:调用 sdsmapchars(mystring, "ho", "01", 2) , "hello" 会转换为 "0ell1" | O(N^2) |
sdsjoin | 将 C 字符串数组连接成一个 SDS 字符串,数组元素间由分隔符 seq 分隔 | O(N^2) |
三、 SDS 特点
常熟复杂度获取字符串长度
SDS 在 len 属性中记录了 SDS 本身的长度,因此获取 SDS 长度的操作,即 sdslen() 的时间复杂度仅为 O(1)。
与 C 字符串相比,获取字符串长度的时间复杂度从 O(N) 降为了 O(1)。API 安全,不会造成缓冲区溢出
在需要对 SDS 进行修改时,API 会对 SDS 的 len 属性和 free 属性进行检查,以确保有足够的空间来满足修改所需的要求,因此不会造成缓冲区溢出问题。-
减少修改字符串时带来的内存重分配次数
因为 Redis 作为数据库,经常用于速度要求严苛、内容修改频繁的场合,所以如果修改字符串时带来的内存重分配次数要尽可能的少,而在 SDS 里采用了空间预分配和惰性空间释放两种策略来进行优化。
通过以上两种策略,可以将 C 字符串的修改字符串长度 N 次必须进行 N 次内存重分配,优化为修改字符串长度 N 次最多需要进行 N 次内存重分配。-
空间预分配
空间预分配,用于优化 SDS 的字符串增长操作。
当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩充时,不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间(即 free 属性的值)。分配的额外空间会受修改后的 SDS 的长度(即 len 属性的值)影响:
- len 小于 1MB :将会给 SDS 分配和 len 属性同样大小的未使用空间,即SDS 的 len 属性值将和 free 属性值相同。
- len 大于等于 1MB :将会给 SDS 分配 1MB 的未使用空间,即 free 属性值等于 1MB。
惰性空间释放
惰性空间释放,用于优化 SDS 的字符串缩短操作。
当 SDS 的 API 需要缩短 SDS 保存的字符串时,并不会立即使用内存重分配来回收多余的字节,而是使用 free 属性来对这些字节数量进行记录。
-
二进制安全
C 字符串中的字符必须符合某种编码,并且除末尾之外,字符串里面不能包含空字符。故而 C 字符串只能用来保存文本数据。
而 SDS 字符串由于不使用空字符,而是使用 len 属性来作为字符串结尾的标志,所以 SDS 的 buf 属性可以用来存储像图像、音频、视频、压缩文件等的二进制数据。
所以说, SDS 的 API 都是二进制安全的(binary-safe)。兼容部分 C 字符串函数
虽然 SDS 的 API 都是二进制安全的,但是由于 SDS 字符串同样遵循 C 字符串以空字符结尾的惯例,所以 SDS 可以重用部分 <string.h> 库定义的函数。