Redis String类型解析
Redis 的 String 类型并不仅仅用于存储文本字符串,它还可以存储数字、二进制数据(如图片序列化后的数据),最大长度可达 512MB。
其核心实现可以概括为两点:
- 动态字符串库:SDS (Simple Dynamic String)
- 智能的编码策略:根据值的内容和长度,选择最节省内存的编码方式
基石:SDS (Simple Dynamic String)
Redis 没有直接使用 C 语言传统的字符串(以空字符 \0 结尾的字符数组),而是自己构建了一种名为 SDS 的抽象类型。所有键和字符串值都是用 SDS 实现的。
SDS 的数据结构 (源码 sds.h)
SDS 的定义非常巧妙,它实际上是一个字节数组(char *),但在其头部隐藏了一个结构体来存储元信息。
1 | /* 注意:这是 Redis 5 及之后版本的结构。在 Redis 3.2 中引入了针对不同长度优化的多种 sdshdr 结构 */ |
__attribute__ ((__packed__)) 告诉编译器不要进行内存对齐,这样可以紧凑地存储结构体,节省每一个字节。
一个 SDS 字符串在内存中的布局如下:
1 | +-------+-------+-------+------+------+ |
上层代码拿到的 sds 类型变量(本质是 char*)直接指向 buf 字段。通过 s[-1] 就可以回溯访问到 flags,从而确定头部的类型和位置,进而获取 len 和 alloc。
SDS 相对于 C 字符串的优势
常数复杂度获取字符串长度 (O(1))
C 语言获取字符串长度需要遍历整个数组,直到遇到\0,时间复杂度为 O(n)。而 SDS 直接将长度存储在len属性中,直接访问即可。1
2
3
4
5
6
7
8
9
10
11static inline size_t sdslen(const sds s) {
// 通过 flags 找到对应的头部,然后返回 len
unsigned char flags = s[-1];
switch(flags & SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8, s)->len; // 直接返回
// ... 其他类型
}
}杜绝缓冲区溢出
C 语言的strcat函数假设目标空间足够,否则会导致溢出。SDS 的 API(如sdscat)在执行修改操作前,会先检查alloc - len是否足够。1
2
3
4
5
6
7
8
9
10
11
12sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s, len); // 关键:检查并扩容!
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}减少修改字符串时带来的内存重分配次数
这是 SDS 最核心的优化。它通过空间预分配和惰性空间释放两种策略来实现。- 空间预分配:当对 SDS 进行扩容时,不仅会分配必需的空间,还会分配额外的未使用空间(
alloc - len)。1
2
3
4
5
6
7
8
9
10
11
12
13
14sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); // alloc - len
if (avail >= addlen) return s; // 空间足够,直接返回
len = sdslen(s);
newlen = (len + addlen);
if (newlen < SDS_MAX_PREALLOC) // SDS_MAX_PREALLOC 是 1MB
newlen *= 2; // 如果新长度小于 1MB,则预分配一倍空间
else
newlen += SDS_MAX_PREALLOC; // 如果新长度大于 1MB,则每次多分配 1MB
// ... 进行重分配操作
} - 惰性空间释放:当缩短 SDS 时,程序不会立即释放多出来的空间,而是通过更新
len将其标记为未使用(alloc不变),留给后续操作使用。当然,也提供了真正的释放 API。
- 空间预分配:当对 SDS 进行扩容时,不仅会分配必需的空间,还会分配额外的未使用空间(
二进制安全
C 字符串以\0作为结束标识,所以不能存储包含空字符的数据(如图片)。SDS 完全依赖len属性来判断字符串是否结束,而不是\0。它的buf是一个字节数组,可以存储任意二进制数据。兼容部分 C 字符串函数
虽然 SDS 是二进制安全的,但它依然会在buf的末尾添加一个\0(alloc空间足够多出一个字节)。这是为了让它的一部分 API 可以直接复用 C 语言的字符串库函数(如printf),避免了不必要的代码重复。
编码策略:redisObject 与 String
在 Redis 中,所有Value都不是直接以SDS的形式存储的,而是被包装在一个叫做 redisObject 的结构体中。这个结构体包含了值的类型、编码方式、实际数据的指针等元信息。
redisObject 结构 (源码 server.h)
1 | typedef struct redisObject { |
对于 String 类型,type 是 OBJ_STRING,但 encoding(编码)可以有三种,这使得 String 的实现非常智能:
OBJ_ENCODING_INT(整数)- 条件:如果字符串对象保存的是一个整数值,并且这个整数值可以用
long类型来表示。 - 实现:此时
ptr指针不再指向一个SDS,而是直接被赋值为这个整数值本身(void *ptr被强制转换存储long类型的值)。 - 优势:节省内存,无需分配额外的SDS结构。并且操作(如
INCR)速度极快。
- 条件:如果字符串对象保存的是一个整数值,并且这个整数值可以用
OBJ_ENCODING_EMBSTR(嵌入的SDS)- 条件:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于
OBJ_ENCODING_EMBSTR_SIZE_LIMIT(在 Redis 中通常是 44 字节)。 - 实现:
redisObject和sdshdr结构会在一次内存分配中连续地分配。ptr指向紧随其后的buf字段。 - 优势:
- 内存效率:只需要一次内存分配,同时分配
redisObject和SDS。释放也只需一次。 - 缓存友好:
redisObject和实际数据紧挨在一起,能更好地利用 CPU 缓存。
- 内存效率:只需要一次内存分配,同时分配
- 条件:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于
OBJ_ENCODING_RAW(原始的SDS)- 条件:如果字符串对象保存的是一个字符串值,且长度大于 44 字节。
- 实现:这就是最常规的实现方式。
redisObject的ptr指针指向一个独立分配的 SDS 结构。 - 优势:对于大字符串,这种分离的分配方式更合适。预分配策略在这里能发挥最大作用。
为什么 EMBSTR 的长度限制是 44 字节?
这是一个为了适配 CPU 缓存行(通常 64 字节)的优化计算。redisObject 结构本身占 16 字节(4bit+4bit+24bit+4bytes+8bytes),sdshdr8 头占 3 字节(uint8_t len + uint8_t alloc + char flags),加上结尾的 \0 占 1 字节。所以 64 - 16 - 3 - 1 = 44 字节。这样整个 robj + sdshdr + buf 可以刚好填充一个 64 字节的缓存行,效率最高。
编码的转换
编码方式并非一成不变,在某些命令下会发生转换:
APPEND命令:对一个OBJ_ENCODING_INT编码的字符串执行APPEND操作,它会首先被转换成OBJ_ENCODING_RAW编码,因为追加后它不再是整数。INCR命令:如果对一个OBJ_ENCODING_EMBSTR或OBJ_ENCODING_RAW编码的、但内容为整数的字符串执行INCR,它可能会被转换为OBJ_ENCODING_INT编码以优化后续操作。
总结
从源码角度看,Redis 的 String 类型是一个工程上的杰作,它通过精巧的数据结构(SDS)和智能的编码策略,在速度、内存效率和功能上取得了完美的平衡:
- SDS 提供了高效的字符串操作(O(1)长度获取、自动扩容、二进制安全),是 Redis 高性能的基石之一。
redisObject提供了统一的接口和丰富的元信息,使得多态命令(如DEL可以处理任何类型)成为可能。- 三种编码(INT, EMBSTR, RAW) 使得 Redis 能够根据存储值的具体情况,选择最节省内存、最快速的表示方式。特别是 EMBSTR 编码,是针对小字符串的极致优化。
这种设计哲学——为不同的数据规模和场景提供不同的底层实现——贯穿了整个 Redis 的所有数据类型,这也是 Redis 为何如此高效和节省内存的重要原因。


