Redis 的简单动态字符串(SDS,Simple Dynamic String)是一种用于表示动态字符串的抽象数据结构。它在 Redis 中被广泛使用,主要用来替代传统的 C 字符串(以 \0 结尾的字符数组)。
以下是它们的主要差异:
1. 数据结构
C 字符串
- 存储方式:C 字符串是一个以 null (
'\0') 结尾的字符数组。 - 长度计算:必须遍历整个字符串直到遇到
'\0'才能知道字符串的长度。
char str[] = "Hello, World!";
Redis SDS
- 存储方式:SDS 是一种自包含的数据结构,不仅存储字符数据,还存储相关元数据。
- 长度计算:通过元数据中的长度字段快速获取字符串的长度,而无需遍历字符数组。
struct sdshdr {
int len; // 当前字符串的长度
int free; // 剩余可用空间的长度
char buf[]; // 实际保存字符串的字符数组
};
2. 内存管理
C 字符串
- 固定分配:内存大小在分配时确定,后续需要手动管理内存(如重新分配)。
- 缓冲区溢出:容易产生缓冲区溢出错误,因为没有额外的机制防止写入超出分配空间。
Redis SDS
- 动态扩展:当字符串长度增加时,SDS 可以自动扩展内存,同时保留一定的预分配空间来减少频繁的内存操作。
- 内存预分配:SDS 采用了惰性空间释放的策略,当字符串长度减少时,不会立即缩小内存,而是保持一定的冗余空间。
3. 性能
C 字符串
- 操作效率低:由于每次获取字符串长度都需要遍历整个字符串,对于长字符串,这个操作开销很大。
- 安全性低:容易出错,需要开发者自己管理内存和考虑边界条件。
Redis SDS
- 操作效率高:字符串长度存储在元数据中,获取长度时间复杂度为 O(1)。
- 安全性高:内置了内存管理机制,避免了常见的缓冲区溢出等问题。
4. 可变性
C 字符串
- 不可变(伪) :C 字符串的大小一旦分配,很难动态调整。在需要修改大小的时候,需要进行复杂的内存重新分配操作。
Redis SDS
- 可变:SDS 本质上是动态可变的,可以方便地增加或减少字符串的长度。并且提供了一些方便的 API 来处理字符串操作。
5. 示例对比
假设我们要将一个字符串 "Hello" 扩展为 "Hello, World!"。
使用 C 字符串
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 初始分配
char *str = (char *)malloc(6);
strcpy(str, "Hello");
// 扩展字符串,需要重新分配内存
str = (char *)realloc(str, 14);
strcat(str, ", World!");
printf("%s\n", str); // 输出: Hello, World!
// 释放内存
free(str);
return 0;
}
使用 Redis SDS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 假设已经定义了 Redis 的 SDS 结构和函数
typedef struct sdshdr {
int len; // 当前字符串的长度
int free; // 剩余可用空间的长度
char buf[]; // 实际保存字符串的字符数组
} sdshdr;
// 创建新 SDS
sdshdr* sdsnew(const char* init) {
size_t initlen = strlen(init);
sdshdr *sh = (sdshdr*)malloc(sizeof(sdshdr) + initlen + 1);
sh->len = initlen;
sh->free = 0;
memcpy(sh->buf, init, initlen + 1);
return sh;
}
// 扩展 SDS
sdshdr* sdscat(sdshdr* s, const char* t) {
size_t len = strlen(t);
// 如果当前的 free 空间不足以容纳新字符串,重新分配内存
if (s->free < len) {
size_t newlen = s->len + len;
s = realloc(s, sizeof(sdshdr) + newlen + 1);
s->free = newlen - s->len;
}
// 将新字符串添加到现有字符串尾部
memcpy(s->buf + s->len, t, len + 1);
s->len += len;
s->free -= len;
return s;
}
// 打印和释放 SDS
void sdsprint(sdshdr* s) {
printf("%s\n", s->buf);
}
void sdsfree(sdshdr* s) {
free(s);
}
int main() {
// 创建一个新的 SDS 字符串
sdshdr* s = sdsnew("Hello");
// 扩展字符串
s = sdscat(s, ", World!");
// 打印结果
sdsprint(s); // 输出: Hello, World!
// 释放内存
sdsfree(s);
return 0;
}
主要区别总结
通过上面的对比和示例,可以更清晰地看到 C 字符串和 Redis SDS 的主要区别:
-
数据结构:
- C 字符串:简单的一维字符数组,以 null (
'\0') 结尾。 - SDS:包含元数据(长度和空闲空间)的结构体,更便于管理和操作。
- C 字符串:简单的一维字符数组,以 null (
-
内存管理:
- C 字符串:需要手动管理内存分配和释放,容易发生缓冲区溢出。
- SDS:自动管理内存扩展和缩减,通过元数据快速获取字符串信息。
-
性能和效率:
- C 字符串:获取长度需要遍历整个字符串,时间复杂度为 O(n)。
- SDS:长度存储在元数据中,获取长度时间复杂度为 O(1)。
-
使用场景:
- C 字符串:适合简单、固定长度的字符串操作。
- SDS:适合需要频繁修改、动态扩展的字符串操作,具有更高的安全性和性能。
总结
Redis 的 SDS 数据结构相比传统的 C 字符串,在内存管理、性能、安全性等方面都有显著优势。这些特点使得 SDS 非常适合在高性能、高并发的环境中使用,比如 Redis 的内部实现。了解这些差异对于理解 Redis 内部工作机制以及优化相关应用是非常重要的。
SDS动态扩容机制
1. 预分配空间策略
当 SDS 增长时,它会预先分配比所需更多的内存,以便将来可能的增长。这种预分配策略基于两种不同的情况:
- 小于1MB的字符串:如果现有字符串的长度小于1MB,当它需要扩展时,SDS 会分配当前长度的两倍大小的内存。例如,如果当前字符串长度为 100 字节,并且需要再追加 50 字节,那么 SDS 会将总容量扩展到 300 字节(150 * 2)。
- 大于等于1MB的字符串:如果现有字符串的长度大于等于1MB,当它需要扩展时,SDS 会一次性分配当前长度加上 1MB 的内存。例如,如果当前字符串长度为 2MB,并且需要再追加 100 字节,那么 SDS 会将总容量扩展到 3MB + 100 字节。
这种预分配策略避免了每次小额增加都重新分配内存,提高了效率。
2. 具体实现
下面是 SDS 扩容的一些关键代码片段和逻辑:
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
// 如果剩余空间已经足够,不需要扩容
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s - sizeof(struct sdshdr);
newlen = (len + addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2; // 小于1MB,则扩展至两倍
else
newlen += SDS_MAX_PREALLOC; // 大于等于1MB,则扩展1MB
newsh = realloc(sh, sizeof(struct sdshdr) + newlen + 1);
if (newsh == NULL) return NULL;
s = (char*)newsh + sizeof(struct sdshdr);
sdssetalloc(s, newlen);
return s;
}
3. 示例说明
假设我们有一个 SDS 字符串,初始内容为 "hello",然后我们进行一系列的追加操作:
sds s = sdsnew("hello"); // 创建一个初始字符串
s = sdscat(s, ", world!"); // 追加字符串,触发扩容
如果 s 初始情况下只有 5 个字符,加上结尾的 \0,总共需要 6 字节。当我们追加 ", world!" 时,总长度变为 13 字节(12字节的字符+1字节的结尾)。
根据上述扩容规则,扩容后容量:12 * 2 + 1 = 25(1字节保存结尾空字符串)。此时内部结构可能如下:
+------+------+
| len | free |
+------+------+
| 13 | 11 |
+---------------------------+
| h | e | l | l | o | , | | w | o | r | l | d | ! | \0 | ... (11) ... |
+---------------------------+
通过这种机制,SDS 可以高效地应对大量的小幅度字符串增长需求,减少频繁的内存分配带来的开销。