Redis 和其他很多 key-value 数据库的不同之处在于,Redis 不仅支持简单的字符串键值对,它 还提供了一系列数据结构类型值,比如列表、哈希、集合和有序集,并在这些数据结构类型上 定义了一套强大的 API 。
1.1 简单动态字符串
Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示,它被用 在几乎所有的 Redis 模块中。
本章将对 sds 的实现、性能和功能等方面进行介绍,并说明 Redis 使用 sds 而不是传统 C 字符 串的原因。
1.1.1 sds 的用途
Sds 在 Redis 中的主要作用有以下两个:
1. 实现字符串对象(StringObject);
2. 在 Redis 程序内部用作 char* 类型的替代品;
1.1.2 Redis 中的字符串
在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。
比如说,hello world 在 C 语言中就可以表示为 “hello world\0” 。
这种简单的字符串表示在大多数情况下都能满足要求,但是,它并不能高效地支持长度计算和 追加(append)这两种操作: • 每次计算字符串长度(strlen(s))的复杂度为 θ(N) 。
对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。
在 Redis 内部,字符串的追加和长度计算并不少见,而 APPEND 和 STRLEN 更是这两种操 作在 Redis 命令中的直接映射,这两个简单的操作不应该成为性能的瓶颈。
另外,Redis 除了处理 C 字符串之外,还需要处理单纯的字节数组,以及服务器协议等内容, 所以为了方便起见,Redis 的字符串表示还应该是二进制安全的:
程序不应对字符串里面保存 的数据做任何假设,数据可以是以 \0 结尾的 C 字符串,也可以是单纯的字节数组,或者其他 格式的数据。 考虑到这两个原因,Redis 使用 sds 类型替换了 C 语言的默认字符串表示:sds 既可以高效地 实现追加和长度计算,并且它还是二进制安全的。
sds 的实现
typedef char *sds;
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
1.1.4 sds 模块的 API
sds 模块基于 sds 类型和 sdshdr 结构提供了以下 API
1.1.5 小结
• Redis 的字符串表示为 sds ,而不是 C 字符串(以 \0 结尾的 char*)。
• 对比 C 字符串,sds 有以下特性: – 可以高效地执行长度计算(strlen);
– 可以高效地执行追加操作(append); – 二进制安全;
• sds 会为追加操作进行优化:加快追加操作的速度,并降低内存分配的次数,
代价是多占 用了一些内存,而且这些内存不会被主动释放。
重要源码解析
sdslen
/* 获取字符串长度 */
static inline size_t sdslen(const sds s) {
// sizeof(struct sdshdr)的值为8
/*
从后面我们可以看到sds指向sdshdr结构的buf[]字符数组,所以
s-(sizeof(struct sdshdr))就是sdshdr结构的地址。
*/
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
sdsavail
/* 获取字符数组中的可用空间 */
static inline size_t sdsavail(const sds s) {
// sizeof(struct sdshdr)的值为8
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
sdsMakeRoomFor
/* 确保sds中的可用空间大于或等于addlen,如果当前字符串可用空间不满足则重新配置空间 */
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)));
// 重新分配空间时并不是分配刚刚好满足需求的空间,而是以其2倍的数量进行分配。
//这点类似于STL中的vector
newlen = (len+addlen);//(当前原有的长度+追加的长度)*2就是我们所需要的
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 调用zrealloc直接在原地进行扩展
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
// 更新可用空间
newsh->free = newlen - len;
return newsh->buf;
}
在目前版本的 Redis 中,SDS_MAX_PREALLOC 的值为 1024 * 1024 ,也就是说,当大小小于 1MB 的字符串执行追加操作时,sdsMakeRoomFor 就为它们分配多于所需大小一倍的空间;当 字符串的大小大于 1MB ,那么 sdsMakeRoomFor 就为它们额外多分配 1MB 的空间。
/* 释放字符数组buf中的多余空间,使其刚好能存放当前字符数 */
sdsRemoveFreeSpace
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
// 重新分配空间,使其刚好能存放当前的字符数量(sizeof(struct sdshdr)+sh->len+1)
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
// 重新分配后当前可用空间为0
sh->free = 0;
return sh->buf;
}
sdsIncrLen
void sdsIncrLen(sds s, int incr) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
// 判断参数incr是否合法,如果不合法说明数据已经发生错误
if (incr >= 0)
assert(sh->free >= (unsigned int)incr);
else
assert(sh->len >= (unsigned int)(-incr));
// 当前长度增加incr
sh->len += incr;
// 可用空间减少incr
sh->free -= incr;
s[sh->len] = '\0';
}
sdsAllocSize
/* 获取sds实际分配的空间大小(包括最后的'\0'结束符) */
size_t sdsAllocSize(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
return sizeof(*sh)+sh->len+sh->free+1;
}