定义
简单动态字符串(SDS)
1 | struct sdshdr { |
用SDS表示 “Redis” 字符串,其中:
- free:值为0,表示该SDS没有任何未分配的空间
- len:值为5,表示该SDS保存了一个五字节长的字符串
- buf:是
char
类型的数组,表示存储的字符串,其中最后一个子节保存了空字符\0
SDS与C字符串
获取字符串长度
由于C字符串并不记录自己的长度信息,所以为了获取一个C字符串的长度,程序必须遍历这个字符串,直到遇到空字符 \0
为止,这个操作的时间复杂度为 O(n)
与C字符串不同,SDS在新增或者更新的时候就会改变 len
的值来记录当前字符串的长度,获取字符串长度直接读取 len
即可,因此复杂度为 O(1),保证了 STRLEN
的性能
杜绝缓冲区溢出
在函数库<string.h>
中的 strcat
函数可以将字符串 src 中的内容拼接到另一个字符串 dest 的末尾 char *strcat(char *dest, const char *src);
假设有两个字符串 S1="Redis"
和字符串 S2="MongoDB"
,并且S1和S2在内存中的地址是相邻的
此时调用 strcat(s1,"Cluster");
,想在S1后追加字符串”Cluster”,但是没有为S1分配组够的空间,就会导致S1的数据溢出到S2中,导致S2的数据意外被修改,如图
而SDS的空间分配策略则杜绝了这种缓冲区溢出的情况:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。
减少字符串分配时带来的内存重分配次数
1. 空间预分配
对于一个长度为N的字符串,C串会为其分配N+1长度字符数组,因此每次字符串需要增长或者缩短时,程序总要为C串重新分配内存。倘若要频繁的操作C串,可能就会导致出现频繁的内存分配操作,对于程序的性能会有一定的影响
Redis是一个对性能要求十分高的一个中间件。对此,当字符空间不足时,SDS会额外的分配未使用空间,而不需要每次增长字符串长度的时候都重新分配空间。相当于使用部分额外的空间来换取内存分配的时间
字符修改后,对于额外空间的数量有下列公式计算
如果
len < 1M
,则程序会分配同样大小的额外空间,此时len == free
如果
len >= 1M
,则程序会分配 1M 的额外空间
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。
2. 惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:当SDS需要缩短保存的字符串的时候,程序并不会立即使用内存重分配来回收多余的字节,而是使用 free 属性来记录多余的字节,以用来之后可能会出现的增长字符串的操作提供一定的空间。
与此同时,SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。
二进制安全
C串中的字符必须要符合某种编码(比如ASCII),并且除了字符末尾外字符中不能包含空字符\0
,否则空字符会被程序误认为是字符串结尾。如图,假设图中是一个连续的字符串,而在读取的时候只能读取到 “Redis” 而无法读取到 “Cluster”
而在SDS结构中,读取字符串不是按照空字符\0
来做判断,而是依据 len
属性来决定字符的长度,因此不会出现上述的问题
不仅能够存储文本数据,SDS也能够保证安全的存储二进制数据,为了确保Redis可以适用于不同的场景,SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
总结
C串 | SDS |
---|---|
获取字符串长度的复杂度为O(n) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度必然需要重新分配内存 | 修改字符串长度不一定要重新分配内存 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
参考 《Redis设计与实现》