简单来说,底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。它们和数据类型的对应关系如下图所示:
String 类型的底层实现只有一种数据结构,也就是简单动态字符串。
简单动态字符串SDS
SDS是"simple dynamic string"的缩写。 Redis中所有场景中出现的字符串,基本都是由SDS来实现的:
-
所有非数字字符串对象的key。例如
set msg "hello world"
中的key msg. -
字符串数据类型的值。例如
set msg "hello world"
中的msg的值"hello wolrd"
-
列表对象类型中的“字符串值”。例如
RPUSH fruits "apple" "banana" "cherry"
中的"apple" "banana" "cherry"
SDS的作用:不仅可以用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区。
(1)SDS的定义
struct sdshdr {
// 记录buf数组中已使用字节的数量,即SDS所保存字符串的长度
unsigned int len;
// 记录buf数据中未使用的字节数量
unsigned int free;
// 字节数组,用于保存字符串
char buf[];
};
free:还剩多少空间 len:字符串长度 buf:存放的字符数组
(2)SDS与C字符串的区别
-
常数复杂度获取字符串长度
C字符串不记录字符串长度,获取长度必须遍历整个字符串,复杂度为O(N);而SDS结构中本身就有记录字符串长度的
len
属性,所有复杂度为O(1)。Redis将获取字符串长度所需的复杂度从O(N)降到了O(1),确保获取字符串长度的工作不会成为Redis的性能瓶颈 -
杜绝缓冲区溢出
C字符串不记录自身的长度,每次增长或缩短一个字符串,都要对底层的字符数组进行一次内存重分配操作。如果是拼接append操作之前没有通过内存重分配来扩展底层数据的空间大小,就会产生缓存区溢出;如果是截断trim操作之后没有通过内存重分配来释放不再使用的空间,就会产生内存泄漏
而SDS通过未使用空间解除了字符串长度和底层数据长度的关联,3.0版本是用
free
属性记录未使用空间,3.2版本则是alloc
属性记录总的分配字节数量。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化的空间分配策略,解决了字符串拼接和截取的空间问题 -
减少修改字符串时带来的内存重分配次数
(1)空间预分配
为减少修改字符串带来的内存重分配次数,SDS采用了“一次管够”的策略:
- 若修改之后SDS长度小于1MB,则多分配现有len属性相同大小的未使用空间,这时SDS的len属性的值将和free属性的值相同
- 若修改之后SDS长度大于等于1MB,则扩充除了满足修改之后的长度外,程序会分配1MB的未使用空间
(2)惰性空间释放
为避免缩短字符串时候的内存重分配操作,SDS在数据减少时,并不立刻释放空间。而是将其作为未使用空间保留在SDS中,如果将来要对SDS进行增长操作的话,这未使用的空间就派上用场。——当然,SDS也有API用于释放未使用的空间。
-
二进制安全
C字符串中的字符必须符合某种编码,除了字符串的末尾,字符串里面是不能包含空字符的,否则会被认为是字符串结尾,这些限制了C字符串只能保存文本数据,而不能保存像图片这样的二进制数据
而SDS的API都会以处理二进制的方式来处理存放在**
buf
属性的字符数组**里的数据,不会对里面的数据做任何的限制。SDS使用len
属性的值来判断字符串是否结束,而不是空字符 -
兼容部分C字符串函数
虽然SDS的API是二进制安全的,但还是像C字符串一样以空字符结尾,目的是为了让保存文本数据的SDS可以重用一部分C字符串的函数
(3)总结:
比起C字符串,SDS具有以下优点:
- 常数复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串长度时所需的内存重分配次数
- 二进制兼容
- 兼容部分C字符串函数
C字符串与SDS对比:
C字符串 | SDS |
---|---|
获取字符串长度复杂度为O(N) | 获取字符串长度复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度必然会需要执行内存重分配 | 修改字符串长度N次最多会需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或二进制数据 |
可以使用所有<string.h> 库中的函数 | 可以使用一部分<string.h> 库中的函数 |
参考:《Redis设计与实现》