Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。在 Redis 里面, C 字符串只会作为字符串字面量(string literal), 用在一些无须对字符串值进行修改的地方, 比如打印日志。当 Redis 需要的不仅仅是一个字符串字面量, 而是一个可以被修改的字符串值时, Redis 就会使用 SDS 来表示字符串值: 比如在 Redis 的数据库里面, 包含字符串值的键值对在底层都是由 SDS 实现的。
redis> SET msg "hello world"
那么 Redis 将在数据库中创建了一个新的键值对, 其中:
键值对的键是一个字符串对象, 对象的底层实现是一个保存着字符串 "msg" 的 SDS 。
键值对的值也是一个字符串对象, 对象的底层实现是一个保存着字符串 "hello world" 的 SDS 。
在redis源码中SDS由struct sdshdr来表示:
struct sdshdr {
int len;
int free;
char buf[];
};
len:字符串的内容的长度
free:空闲空间的长度
char:放置字符串的内容的指针
reids里面会针对字符串内容的长度分配一定的空间由buf进行指向,在这段分配的空间中,len是实际字符串的长度,free是剩余的空闲空间的长度,注意的是字符串结束符号'\0'既不是len里面的,也不是free里面的。例如下面:
SDS与C字符串的区别:
1.用一个字段记录下字符串的长度使得获取字符串的长度的时间复杂度为O(1),
2.用一个free字段记录下分配空间的剩余空闲空间可以防止字符串在长度增加的过程中造成的缓冲区溢出
3.避免了频繁的内存重分配,在C语言中每次字符串长度变化的时候,都要进行内存的重新分配,而SDS中字符串的长度在不超过len+free长度的时候只需要进行覆盖就行了,不需要进行重新分配。
SDS空间预分配
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen)
return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL)
return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
s:操作的sds
addlen:指在原来的字符串的基础上,需要增加的长度,而不是硬性的在原来的分配的空间上需要增加的长度
sdsMakeRoomFor:首先对s的free与addlen进行比较,如果free>=addlen,也就是原来的空间足够容纳下增加的长度,那么直接返回,不需要进行空间的分配。如果free<addlen,也就是原来的空间不足以容纳增加的长度了,那么需要计算分配的大小的长度。newlen计算出新的字符串的长度,然后根据这个值的大小进行不同的分配:如果对 SDS 进行修改之后, SDS 的长度将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同,也就是free = len = newlen。如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间,也就是free = 1M,len = newlen。 那么总的分配的空间将会是total = len + free + 1,最后的1是'\0',要注意的是,在重新分配之后已经完成了旧的内容的迁移。
空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。如果真的有需要对空间进行回收,下面的函数就是将free设置为0,让空间刚好能够盛装下字符串。
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
sh->free = 0;
return sh->buf;
}
sds:操作的sds
现在字符串需要的空间是len+1,就按照这个大小对大小进行重新分配,free=0。要注意的是在重新分配的过程中已经完成的旧的内容的迁移。
二进制安全
C 字符串中的字符必须符合某种编码, 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据,因为空字符被认为是C字符串的结束。虽然数据库一般用于保存文本数据, 但使用数据库来保存二进制数据的场景也不少见, 因此, 为了确保 Redis 可以适用于各种不同的使用场景, SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。这也是我们将 SDS 的 buf 属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据。比如说, 使用 SDS 来保存之前提到的特殊数据格式就没有任何问题, 因为 SDS 使用 len 属性的值而不是空字符来决定字符串是否结束。
兼容C字符串函数
虽然 SDS 的 API 都是二进制安全的, 但它们一样遵循 C 字符串以空字符结尾的惯例: 这些 API 总会将 SDS 保存的数据的末尾设置为空字符, 并且总会在为 buf 数组分配空间时多分配一个字节来容纳这个空字符, 这是为了让那些保存文本数据的 SDS 可以重用一部分 <string.h> 库定义的函数。