Skip to content

redis数据结构之字符串 #10

@lee3164

Description

@lee3164

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    redisarticle about redis code, operation, etc.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions