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 的原因
- 常数复杂度获取字符串长度:获取 SDS 字符串长度的操作时间复杂度为 \(O(1)\),因为
len
字段已经记录了字符串的长度。而传统 C 字符串获取长度需要遍历整个字符串,时间复杂度为 \(O(N)\),使用 SDS 可以确保获取字符串长度的操作不会成为 Redis 的性能瓶颈。 - 杜绝缓冲区溢出:C 字符串不记录自身长度和空闲空间,在进行字符串拼接等操作时容易造成缓冲区溢出。而 SDS 在拼接字符串之前会先通过
free
字段检测剩余空间能否满足需求,如果不足则会进行扩容,从而避免了缓冲区溢出的问题。 - 减少修改字符串时的内存重分配次数:
- 空间预分配:在对 SDS 进行扩展时,程序不仅会为 SDS 分配修改所必须的空间,还会分配额外的未使用空间。这样可以减少连续执行字符串增长操作所需的内存重分配次数,将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。
- 惰性空间释放:在对 SDS 进行缩短操作时,程序不会立刻使用内存重分配来回收缩短之后多出来的字节,而是通过
free
属性将这些字节的数量记录下来,等待将来使用。这避免了缩短字符串时所需的内存重分配次数,并且为将来可能的增长操作提供了优化。
- 二进制安全:SDS 的 API 都是二进制安全的,所有 API 都会以处理二进制的方式来处理存放在
buf
数组里的数据,程序不会对其中的数据做任何限制、过滤,数据存进去是什么样子,读出来就是什么样子。因此 Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据。
四、SDS 主要 API 及其源码解析
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
指针。
sdsempty
函数:创建一个不包含任何内容的 SDS。
sds sdsempty(void) {return sdsnewlen("", 0);
}
该函数简单地调用 sdsnewlen
函数,传入空字符串和长度 0 来创建一个空的 SDS。
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
函数释放内存。
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
字段值,由于是内联函数,提高了多次调用时的效率。
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
字段值。
sdsdup
函数:创建一个给定 SDS 的副本。
sds sdsdup(const sds s) {return sdsnewlen(s, sdslen(s));
}
该函数调用 sdsnewlen
函数和 sdslen
函数,根据传入的 SDS s
的内容和长度创建一个新的 SDS 副本。
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
属性。
sdscat
函数:将给定的 C 字符串拼接到 SDS 字符串的末尾。
sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}
该函数调用 sdscatlen
函数,并传入 s
、t
和 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
属性。
sdscatsds
函数:将给定的 SDS 字符串拼接到另一个 SDS 字符串的末尾。
sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));
}
该函数调用 sdscatlen
函数和 sdslen
函数,将 t
拼接到 s
的末尾。
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。
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
属性。
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
属性。