Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis 的默认字符串表示。
在 Redis 中,C 字符串只会作为字符串字面量用在一些无需对字符串进行修改的地方,比如打印日志:
serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
当 Redis 需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会适应 SDS 来表示字符串。比如在数据库中,包含字符串值的键值对在底层都是由 SDS 实现的。
还是拿简单的 SET 命令举例,执行以下命令
redis> SET msg "hello world"
ok
那么,Redis 将在数据中创建一个新的键值对,其中:
- 键值对的键是一个字符串对着,对象的底层实现是一个保存着字符串 "msg" 的 SDS。
- 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串 "hello world" 的 SDS。
除了用来保存数据库中的字符串值之外, SDS 还被用作缓冲区。AOF 模块中的 AOF 缓冲区,以及客户端状态中的输入缓冲区,都是由 SDS 实现的。
接下来,我们就来详细认识下 SDS。
1 SDS 的定义
在 sds.h 中,我们会看到以下结构:
typedef char *sds;
可以看到,SDS 等同于 char * 类型。这是因为 SDS 需要和传统的 C 字符串保存兼容,因此将其类型设置为 char *。但是要注意的是,SDS 并不等同 char *,它还包括一个 header 结构,共有 5 中类型的 header,源码如下:
struct __attribute__ ((__packed__)) sdshdr5 { // 已弃用
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 长度小于 2^8 的字符串类型
uint8_t len; // SDS 所保存的字符串长度
uint8_t alloc; // SDS 分配的长度
unsigned char flags; // 标记位,占 1 字节,使用低 3 位存储 SDS 的 type,高 5 位不使用
char buf[]; // 存储的真实字符串数据
};
struct __attribute__ ((__packed__)) sdshdr16 { // 长度小于 2^16 的字符串类型
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 长度小于 2^32 的字符串类型
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 { // 长度小于 2^64 的字符串类型
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
之所以会有 5 种类型的 header,是为了能让不同长度的字符串使用对应大小的 header,提高内存利用率。
一个 SDS 的完整结构,由内存地址上前后相邻的两部分组成:
- header:包括字符串的长度(len),最大容量(alloc)和 flags(不包含 sdshdr5)。
- buf[]:一个字符串数组。这个数组的长度等于最大容量加 1,存储着真正的字符串数据。
图 1-1 展示了一个 SDS 示例:
示例中,各字段说明如下:
- alloca:SDS 分配的空间大小。图中表示分配的空间大小为 10。
- len:SDS 保存字符串大小。图中表示保存了 5 个字节的字符串。
- buf[]:这个数组的长度等于最大容量加 1,存储着真正的字符串数据。图中表示数字的前 5 个字节分别保存了 'H'、'e'、'l'、'l'、'o' 五个字符,而最后一个字节则保存了空字符串 ' '。
SDS 遵循 C 字符串以空字符结尾的惯例,保存空字符的大小不计算在 SDS 的 len 属性中。此外,添加空字符串到字符串末尾等操作,都是由 SDS 函数(sds.c 文件中的相关函数)自动完成的。
而且,遵循空字符结尾的惯例,还可以直接重用一部分 C 字符串函数库中的函数。
例如,我们可以直接使用 printf()
函数打印 s->buf
:
printf("%s", s->buf);
这样,我们可以直接使用 C 函数来打印字符串 "Redis",无需为 SDS 编写转码的打印函数。
2 SDS 对比 C 字符串有哪些优势
在 C 语言中,使用长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组的最后一个元素总是空字符 "