Redis/简单动态字符串
Redis内部使用的字符串是自己构建的简单动态字符串(SDS)的抽象类型,C字符串只作为字符串字面量使用
创建KV时,有两种情况:
- V是字符串对象,则底层实现是 SDS:SDS
- V是列表,则实现是SDS:SDS,SDS,…
SDS的作用
- 保存数据库中的字符串中
- 用作缓冲区(buffer),例如AOF缓冲区和客户端状态中的输入缓冲区
SDS的定义
SDS的值存储在sds.h中的sdshdr
结构体
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用来保存字符串
char buf[];
}
SDS的字符串遵守C字符串以空字符结尾的管理,因此可以按照C字符串打印的方式直接打印出来,如:
printf("%s", s->buf);
这样一来,就不需要编写专门输出SDS的输出函数了
同样的,SDS还可以重用一部分其他的C字符串函数库里的函数
SDS与C字符串的区别
C字符串的表示方式比较简单,不能满足Redis对字符串在安全性、效率以及功能方面的需求,因此才诞生了SDS,SDS的优势有以下几个方面
常数复杂度获取字符串长度
SDS维护了一个len
变量用以存储字符串长度,而不需要像C字符串一样遍历整个字符串
设置和更新len
是由SDS的API执行时自动完成的
杜绝缓冲区溢出
同样的,C字符串不记录长度也使得它易发生缓冲区溢出,而SDS维护的长度变量则可以杜绝这个问题。
当需要对SDS进行修改时,API会先检查SDS的空间是否满足修改需求,如果不满足则会自动将SDS的空间扩展至执行修改所需的大小
// sdscat 是SDS提供的用于执行拼接操作的函数
sdscat(s, "Cluster");
sdscat
会先检查给定SDS的空间是否足够,如果不够,就会先扩展SDS空间,然后执行拼接操作
减少修改字符串时带来的内存重分配次数
C字符串每次增长或缩短,总要进行一次内存重分配操作:
- 增长(例如拼接)操作,那么执行该操作之前就必须通过内存重分配来扩展底层数组空间大小,否则就会产生缓冲区溢出
- 缩短(例如截断)操作,那么就需要内存重分配来释放不再使用的空间,否则会产生内存泄漏
内存重分配涉及复杂的算法,且有可能需要执行系统调用,因此是一个比较耗时的操作,频繁的内存重分配对Redis来说是不能容忍的
Redis为了规避C字符串的这种缺陷,让 SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联
在SDS中,buf
数组可以包含未使用的字节,free
变量则负责记录未使用的字节数量
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略
1.空间预分配
用于优化SDS的字符串增长操作。
当SDS的API对一个SDS进行修改,并且需要扩展空间时,程序不仅为SDS分配修改所必须的空间,还会分配额外的未使用空间
额外分配的未使用空间数量由以下公式决定:
- 如果SDS修改后,长度(即
len
的值)将小于1MB,则分配和len
同样大小的未使用空间,即len
和free
的大小将会变为一致 - 如果SDS修改后,长度将大于1MB,则分配1MB的未使用空间
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数
2.惰性空间释放
用于优化SDS的字符串缩短操作
当SDS的API需要缩短SDS保存的字符串时,程序并不立刻回收缩短后多出来的字节,而是使用free
属性记录这些字节的数量,以期未来使用
同时,SDS也提供了相应API,保证在有需要的时候真正释放SDS的未使用空间
二进制安全
C语言中字符串中的字符必须符合某种编码,并且只有字符串末尾才能包含空字符,否则会被认为提前结束,因此C字符串只能保存文本数据,而不能保存图片、音频、压缩文件等二进制数据
SDS的API则是二进制安全的,所有SDS API都会以处理二进制的方式来处理buf里的数据,字符串的长度靠len
保证,因此SDS可以保存文本数据及任意格式的二进制数据
兼容部分C字符串函数
SDS的API总会将SDS保存的数据的末尾设置为空字符(并为其多分配一个字节的空间),是为了让保存文本数据的SDS可以重用一部分C字符串库中的函数
C字符串与SDS的区别 - 小结
C字符串 | SDS |
---|---|
获取字符串长度的时间复杂度为O(N) | 获取字符串长度的时间复杂度为O(1) |
API不安全,可能造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
修改字符串长度N次必须执行N次内存重分配 | 修改字符串长度N次最多执行N次内存重分配 |
只能保存文本数据 | 可以保存文本数据和二进制数据 |
可以使用所有<string.h>库的函数 | 保存文本数据的SDS可以使用一部分<string.h>库的函数 |
SDS API
小结
Redis只会使用C字符串作为字面量,大部分情况下使用SDS作为字符串
相比起C字符串,SDS有以下几个优点:
- 常数时间复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串长度所需的内存分配次数,通过预分配空间和惰性释放空间来实现
- 二进制安全
- 兼容一部分C字符串函数