-
Notifications
You must be signed in to change notification settings - Fork 0
Description
redis数据结构之字符串
标签(空格分隔): redis
定义
typedef char *sds;
struct sdshdr {//该结构一般分配过程可以参考createStringObject
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[]; //可以参考sdsnewlen
};这个定义和nginx中字符串的定义其实很类似,其中sdshdr数据结构创建之后会将buf属性当做sds返回,通过sds获取sdshdr的方式如下,其实就是简单的指针操作。
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));可能很多人会好奇这里为啥是s-sizeof(struct sdshdr),s在这里不是buf的起始地址吗?如果在减去一整个struct的长度不就过头了吗?原因在buf的声明中这个是一个柔性数组,详细见柔细数组的介绍。
为什么不用c字符串?
C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符
将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
举个例子, 如果有一种使用空字符来分割多个单词的特殊数据格式(Redis\0Cluster\0), 那么这种格式就不能使用 C 字符串来保存, 因为C
字符串所用的函数只会识别出其中的 "Redis" , 而忽略之后的 "Cluster" 。
虽然数据库一般用于保存文本数据, 但使用数据库来保存二进制数据的场景也不少见, 因此, 为了确保 Redis 可以适用于各种不同的使用场景,
SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对
其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。
这也是我们将 SDS 的 buf 属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据。
相关操作
| 函数 | 作用 | 时间复杂度 |
|---|---|---|
| sdsnew | 创建一个包含给定 C 字符串的 SDS | O(N) ,N 为给定C字符串的长度 |
| sdsempty | 创建一个不包含任何内容的空 SDS | O(1) |
| sdsfree | 释放给定的 SDS | 这个值可以通过读取 SDS 的 len 属性来直接获得, 复杂度为 O(1) |
| sdsavail | 返回 SDS 的未使用空间字节数 | 这个值可以通过读取 SDS 的 free 属性来直接获得, 复杂度为 O(1) |
| sdsdup | 创建一个给定 SDS 的副本(copy) | O(N) , N 为给定 SDS 的长度 |
| sdsclear | 清空SDS保存的字符串内容 | 因为惰性删除,时间复杂度为O(1) |
| sdscat | 将c字符串拼接到sds末尾 | O(N),N为拼接字符串的长度 |
| sdscatsds | 拼接sds字符串 | O(N),N为拼接SDS字符串长度 |
| sdscpy | 将C字符串复制到SDS里面,覆盖SDS原先的内容 | O(N) |
| sdsgrowzero | 用空字符串扩展sds到指定长度 | O(N) |
| sdsrange | 保留SDS给定区间内的数据,不在区间的会被覆盖或清除 | O(N) |
| sdstrim | 接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在 C 字符串中出现过的字符。 | O(M*N) , M 为 SDS 的长度, N 为给定 C 字符串的长度。 |
| sdscmp | 对比两个SDS字符串是否相同 | O(N) |
空间预分配
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
其中, 额外分配的未使用空间数量由以下公式决定:如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 1 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte 。通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。 通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。 见sdscat
#define SDS_MAX_PREALLOC (1024*1024)
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;
}