动态字符串SDS
redis中的所有key是字符串,所有value本质上也是字符串,比如 集合set中的每一个 成员 都是一个独立的字符串对象,列表中的每一个 元素 都是一个独立的字符串对象,整个HASH是一个对象,它内部的每一个 字段(field) 和一个字段值(value) 都是一个独立的字符串对象
redis是通过c语言来实现的,但是没有直接使用c语言中的字符串,有几下几点原因
- 获取字符串长度需要通过运算:C字符串以
\0
(空字符)结尾,要获取长度必须遍历整个数组直到遇到\0
,时间复杂度为O(n),这在高性能数据库如Redis中效率低下。 - 非二进制安全:C字符串不能存储任意二进制数据,因为它依赖于
\0
作为结束符。如果数据中包含\0
(如一些二进制文件),会被错误截断,破坏数据完整性。 - 不可修改:C语言字符串常量(如
char* s = "hello"
)是只读的,无法直接扩展或修改其长度,这在动态数据存储中不灵活。
Redis的解决方案:Redis因此构建了自己的字符串结构——SDS(简单动态字符串),它通过设计一个智能结构来支持查找、二进制安全性和动态修改。
SDS底层数据结构
uint8_t
(8位无符号整数),可表示的最大值是 255 (因为2^8 - 1 = 255
),因此len
最多记录 255 字节 的长度,否则会溢出,如果一个 SDS 字符串的实际长度超过 255 字节,Redis 会自动选择更大容量的结构体(如sdshdr16
/sdshdr32
)。
falgs :类型自动匹配:SDS 根据字符串长度动态选择头类型
#define SDS_TYPE_5 0 // 超短字符串(≤31B)
#define SDS_TYPE_8 1 // 短字符串(≤255B)
#define SDS_TYPE_16 2 // ≤65535B
#define SDS_TYPE_32 3 // ≤4GB
#define SDS_TYPE_64 4 // 超大字符串
为了和c语言兼容会在结尾带上\0,但是我们的sds再去读取这个字符串的时候不会以结束标识作为标准来读,因为已经有长度了,会从开始读len个长度停止
SDS特性
-
初始状态:存储值
"hi"
时,SDS 头部记录len=2, alloc=2
(分配空间=实际占用)| len: alloc:2 | flags:1 | h | i | \0 |
-
追加数据
",Amy"
:- 新长度 = 2+4=6(需扩容)
- 内存预分配规则触发:
- 若新增长度 <1MB → 分配 2倍新长度+1
- 若新增长度 ≥1MB → 分配 新长度+1MB+1
- 本例中
6 < 1MB
→ 新空间为6*2+1=13字节
→ 记录alloc=12
(不含结尾\0
)
为什么需要内存预分配?
-
优化高频修改性能
- 若每次追加字符串都精确分配空间(如
len=6 → alloc=6
),后续继续追加时需重复申请内存 - 预分配额外空间 让连续多次追加(如循环中逐步构建字符串)不再触发扩容操作
- 若每次追加字符串都精确分配空间(如
- 减少内核态和用户态的转换(申请内存空间的时候是需要切换到内核态的)
- 场景对比:
- 未预分配:追加
"A"+"B"+"C"
→ 发生3次realloc()
→ 3次系统调用(最差情况) - 预分配后:追加
"A"+"B"+"C"
→ 仅首次需扩容时触发了1次realloc()
→ 其余操作直接写入预留空间→ 减少66%的系统调用数
- 未预分配:追加
- 场景对比:
alloc申请的长度不包括最后的结束标识符,所以这里是12