为什么使用 SDS(Simple Dynamic String,动态字符串)
- 常数复杂度获取字符串长度 O(1)
- 杜绝缓冲区溢出
- 减少修改字符串时带来的内存重分配次数(最多扩容 n 次,空间预分配、惰性空间释放)
- 二进制安全,SDS API 会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据( buf 里保存的是二进制数据)
知识点
[注] 相关源码保存位置:src/redis.h、src/sds.h、src/sds.c。
-
Redis 只会使用 C 字符串作为字面量
-
在 redis 中字符串叫 SDS(Simple Dynamic String,动态字符串)
-
SDS 结构如下 – 是一个带长度信息的字节数组。
struct SDS<T> { T capacity; // 数组容量,1byte T len; // 数组长度,1byte byte flags; // 特殊标识位,1byte byte[] content; // 数组内容 }
-
字符串存储方式分为
- embstr 长度 44byte 及以下
- raw 长度 45byte 及以上
-
embstr 最大长度为 44byte 的原因如下
- 所有的 Redis 对象都有下面的这个结构头,此结构占据 16byte 的存储空间
struct RedisObject { int4 type; // 4bits int4 encoding; // 4bits int24 lru; // 24bits int32 refcount; // 4bytes void *ptr; // 8bytes,64-bit system } robj;
- 此外 SDS 结构体至少要占用
3byte
的空间,所以分配一个字符串的最小空间占用为 19byte (16+3) - 内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32byte 的空间,如果字符串再稍微长一点,那就是 64byte 的空间。如果总体超出了 64byte,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。
- SDS 结构体中的 content 中的字符串是以字节
\0
结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。
所以 embstr 最大能容纳的字符串长度就是 44byte (64-16-3-1)。
- 所有的 Redis 对象都有下面的这个结构头,此结构占据 16byte 的存储空间
扩容策略
- 字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。
- 当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。
C 字符串和 SDS 之间的区别
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度为 O(N) | 获取字符串长度的复杂度为 O(1) |
API 是不安全的,可能会造成缓冲区溢出 | API 是安全的,不会造成缓冲区溢出 |
修改字符串长度 N 次必然需要执行 N 次内存重分配 | 修改字符串长度 N 次,最多需要执行 N 次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有 <string.h> 库中的函数 | 可以使用一部分 <string.h> 库中的函数 |